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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { SessionManager } from "../live/session.js";
|
|
2
|
+
import { getStrategy } from "../strategies/index.js";
|
|
3
|
+
|
|
4
|
+
// Module-level shared SessionManager for the MCP server process
|
|
5
|
+
const manager = new SessionManager();
|
|
6
|
+
|
|
7
|
+
function requireSession(sessionId) {
|
|
8
|
+
const s = manager.get(sessionId);
|
|
9
|
+
if (!s) throw new Error(`No session found with id "${sessionId}"`);
|
|
10
|
+
return s;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { manager as sessionManager };
|
|
14
|
+
|
|
15
|
+
export const liveTools = {
|
|
16
|
+
create_session: {
|
|
17
|
+
description:
|
|
18
|
+
"Create a new paper (default) or live (gated) trading session. Paper needs no credentials.",
|
|
19
|
+
handler: async ({
|
|
20
|
+
sessionId,
|
|
21
|
+
symbol,
|
|
22
|
+
mode = "paper",
|
|
23
|
+
interval = "1m",
|
|
24
|
+
equity = 10_000,
|
|
25
|
+
riskPct,
|
|
26
|
+
maxDailyLossPct,
|
|
27
|
+
confirmLive = false,
|
|
28
|
+
} = {}) => {
|
|
29
|
+
const session = await manager.create({
|
|
30
|
+
id: sessionId,
|
|
31
|
+
symbol,
|
|
32
|
+
mode,
|
|
33
|
+
interval,
|
|
34
|
+
equity,
|
|
35
|
+
...(riskPct != null ? { riskPct } : {}),
|
|
36
|
+
...(maxDailyLossPct != null ? { maxDailyLossPct } : {}),
|
|
37
|
+
confirmLive,
|
|
38
|
+
});
|
|
39
|
+
return session.getStatus();
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
list_sessions: {
|
|
44
|
+
description: "List all active trading sessions and their current status.",
|
|
45
|
+
handler: async () => {
|
|
46
|
+
return manager.list().map((s) => s.getStatus());
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
session_status: {
|
|
51
|
+
description:
|
|
52
|
+
"Get a full refreshed status snapshot for a session (positions, orders, equity, risk).",
|
|
53
|
+
handler: async ({ sessionId } = {}) => {
|
|
54
|
+
const session = requireSession(sessionId);
|
|
55
|
+
return session.refresh();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
feed_price: {
|
|
60
|
+
description:
|
|
61
|
+
"Feed a price bar (or single price) to a session, advancing paper simulations and triggering fills.",
|
|
62
|
+
handler: async ({ sessionId, bar, price } = {}) => {
|
|
63
|
+
const session = requireSession(sessionId);
|
|
64
|
+
let b = bar;
|
|
65
|
+
if (!b && Number.isFinite(price)) {
|
|
66
|
+
b = { time: Date.now(), open: price, high: price, low: price, close: price, volume: 0 };
|
|
67
|
+
}
|
|
68
|
+
if (!b) throw new Error("Provide either `bar` (OHLCV) or `price` (number)");
|
|
69
|
+
await session.pushBar(b);
|
|
70
|
+
|
|
71
|
+
// If a strategy is attached and session is flat, evaluate it
|
|
72
|
+
if (session._strategy && session.getStatus().positions.length === 0) {
|
|
73
|
+
try {
|
|
74
|
+
const signal = session._strategy(session.candleBuffer, session.getStatus());
|
|
75
|
+
if (signal && signal.side && signal.type) {
|
|
76
|
+
await session.placeOrder(signal).catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// strategy errors are non-fatal
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return session.getStatus();
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
place_order: {
|
|
88
|
+
description:
|
|
89
|
+
"Place a market or limit order in a session (optionally risk-sized with bracket stop/target).",
|
|
90
|
+
handler: async ({
|
|
91
|
+
sessionId,
|
|
92
|
+
side,
|
|
93
|
+
type = "market",
|
|
94
|
+
qty,
|
|
95
|
+
riskPct,
|
|
96
|
+
stop,
|
|
97
|
+
target,
|
|
98
|
+
rr,
|
|
99
|
+
limitPrice,
|
|
100
|
+
} = {}) => {
|
|
101
|
+
const session = requireSession(sessionId);
|
|
102
|
+
return session.placeOrder({ side, type, qty, riskPct, stop, target, rr, limitPrice });
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
close_position: {
|
|
107
|
+
description: "Close the open position for a symbol in a session via an opposite market order.",
|
|
108
|
+
handler: async ({ sessionId, symbol } = {}) => {
|
|
109
|
+
const session = requireSession(sessionId);
|
|
110
|
+
return session.closePosition(symbol);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
flatten: {
|
|
115
|
+
description: "Flatten all positions and cancel all open orders in a session.",
|
|
116
|
+
handler: async ({ sessionId } = {}) => {
|
|
117
|
+
const session = requireSession(sessionId);
|
|
118
|
+
await session.flatten();
|
|
119
|
+
return { ok: true };
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
cancel_order: {
|
|
124
|
+
description: "Cancel a specific open order in a session.",
|
|
125
|
+
handler: async ({ sessionId, orderId } = {}) => {
|
|
126
|
+
const session = requireSession(sessionId);
|
|
127
|
+
await session.cancelOrder(orderId);
|
|
128
|
+
return { ok: true };
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
account: {
|
|
133
|
+
description: "Get the broker account details for a session (equity, cash, buying power).",
|
|
134
|
+
handler: async ({ sessionId } = {}) => {
|
|
135
|
+
const session = requireSession(sessionId);
|
|
136
|
+
return session.getAccount();
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
positions: {
|
|
141
|
+
description: "Get all open positions for a session.",
|
|
142
|
+
handler: async ({ sessionId } = {}) => {
|
|
143
|
+
const session = requireSession(sessionId);
|
|
144
|
+
return session.getPositions();
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
recent_events: {
|
|
149
|
+
description: "Get recent session events (fills, risk changes, bars) for monitoring.",
|
|
150
|
+
handler: async ({ sessionId, limit = 50 } = {}) => {
|
|
151
|
+
const session = requireSession(sessionId);
|
|
152
|
+
return session.recentEvents(limit);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
attach_strategy: {
|
|
157
|
+
description:
|
|
158
|
+
"Attach a named built-in strategy to a session. It will auto-evaluate on each feed_price and place orders when flat.",
|
|
159
|
+
handler: async ({ sessionId, strategy, params = {} } = {}) => {
|
|
160
|
+
const session = requireSession(sessionId);
|
|
161
|
+
const factory = getStrategy(strategy);
|
|
162
|
+
const signal = factory(params);
|
|
163
|
+
// Wrap: accept (candleBuffer, status) and call signal with the buffer
|
|
164
|
+
session._strategy = (candleBuffer) => {
|
|
165
|
+
if (!candleBuffer || candleBuffer.length === 0) return null;
|
|
166
|
+
return signal(candleBuffer[candleBuffer.length - 1], candleBuffer);
|
|
167
|
+
};
|
|
168
|
+
return { ok: true, strategy, params };
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
halt_all: {
|
|
173
|
+
description: "Emergency kill switch: flatten all positions and stop all trading sessions.",
|
|
174
|
+
handler: async () => {
|
|
175
|
+
await manager.haltAll();
|
|
176
|
+
return { ok: true, sessionsHalted: manager.list().length };
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
package/src/mcp/schemas.js
CHANGED
|
@@ -20,6 +20,19 @@ const dataSpec = z
|
|
|
20
20
|
})
|
|
21
21
|
.passthrough();
|
|
22
22
|
|
|
23
|
+
const barShape = z.object({
|
|
24
|
+
time: z.number(),
|
|
25
|
+
open: z.number().optional(),
|
|
26
|
+
high: z.number().optional(),
|
|
27
|
+
low: z.number().optional(),
|
|
28
|
+
close: z.number(),
|
|
29
|
+
volume: z.number().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const sessionMode = z.enum(["paper", "live"]).optional();
|
|
33
|
+
const orderSide = z.enum(["long", "short", "buy", "sell"]);
|
|
34
|
+
const orderType = z.enum(["market", "limit", "stop", "stop_limit"]).optional();
|
|
35
|
+
|
|
23
36
|
export const schemas = {
|
|
24
37
|
list_strategies: {},
|
|
25
38
|
fetch_candles: dataSpec.shape,
|
|
@@ -45,4 +58,110 @@ export const schemas = {
|
|
|
45
58
|
grid: z.record(z.string(), z.array(z.any())).optional(),
|
|
46
59
|
backtestOptions: z.record(z.string(), z.any()).optional(),
|
|
47
60
|
},
|
|
61
|
+
|
|
62
|
+
// Research-plus tools
|
|
63
|
+
analyze_robustness: {
|
|
64
|
+
candles: z.array(candle).optional(),
|
|
65
|
+
data: dataSpec.optional(),
|
|
66
|
+
symbol: z.string().optional(),
|
|
67
|
+
interval: z.string().optional(),
|
|
68
|
+
strategy: z.string(),
|
|
69
|
+
params: z.record(z.string(), z.any()).optional(),
|
|
70
|
+
equityStart: z.number().optional(),
|
|
71
|
+
iterations: z.number().optional(),
|
|
72
|
+
blockSize: z.number().optional(),
|
|
73
|
+
seed: z.union([z.string(), z.number()]).optional(),
|
|
74
|
+
numTrials: z.number().optional(),
|
|
75
|
+
sharpeStd: z.number().optional(),
|
|
76
|
+
skew: z.number().optional(),
|
|
77
|
+
kurtosis: z.number().optional(),
|
|
78
|
+
backtestOptions: z.record(z.string(), z.any()).optional(),
|
|
79
|
+
},
|
|
80
|
+
optimize_strategy: {
|
|
81
|
+
candles: z.array(candle).optional(),
|
|
82
|
+
data: dataSpec.optional(),
|
|
83
|
+
symbol: z.string().optional(),
|
|
84
|
+
interval: z.string().optional(),
|
|
85
|
+
strategy: z.string(),
|
|
86
|
+
grid: z.record(z.string(), z.array(z.any())).optional(),
|
|
87
|
+
scoreBy: z.string().optional(),
|
|
88
|
+
backtestOptions: z.record(z.string(), z.any()).optional(),
|
|
89
|
+
},
|
|
90
|
+
compare_strategies: {
|
|
91
|
+
candles: z.array(candle).optional(),
|
|
92
|
+
data: dataSpec.optional(),
|
|
93
|
+
symbol: z.string().optional(),
|
|
94
|
+
interval: z.string().optional(),
|
|
95
|
+
strategies: z.array(
|
|
96
|
+
z.object({
|
|
97
|
+
strategy: z.string(),
|
|
98
|
+
params: z.record(z.string(), z.any()).optional(),
|
|
99
|
+
})
|
|
100
|
+
),
|
|
101
|
+
scoreBy: z.string().optional(),
|
|
102
|
+
backtestOptions: z.record(z.string(), z.any()).optional(),
|
|
103
|
+
},
|
|
104
|
+
candle_stats: {
|
|
105
|
+
candles: z.array(candle).optional(),
|
|
106
|
+
data: dataSpec.optional(),
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Live trading tools
|
|
110
|
+
create_session: {
|
|
111
|
+
sessionId: z.string(),
|
|
112
|
+
symbol: z.string(),
|
|
113
|
+
mode: sessionMode,
|
|
114
|
+
interval: z.string().optional(),
|
|
115
|
+
equity: z.number().optional(),
|
|
116
|
+
riskPct: z.number().optional(),
|
|
117
|
+
maxDailyLossPct: z.number().optional(),
|
|
118
|
+
confirmLive: z.boolean().optional(),
|
|
119
|
+
},
|
|
120
|
+
list_sessions: {},
|
|
121
|
+
session_status: {
|
|
122
|
+
sessionId: z.string(),
|
|
123
|
+
},
|
|
124
|
+
feed_price: {
|
|
125
|
+
sessionId: z.string(),
|
|
126
|
+
bar: barShape.optional(),
|
|
127
|
+
price: z.number().optional(),
|
|
128
|
+
},
|
|
129
|
+
place_order: {
|
|
130
|
+
sessionId: z.string(),
|
|
131
|
+
side: orderSide,
|
|
132
|
+
type: orderType,
|
|
133
|
+
qty: z.number().optional(),
|
|
134
|
+
riskPct: z.number().optional(),
|
|
135
|
+
stop: z.number().optional(),
|
|
136
|
+
target: z.number().optional(),
|
|
137
|
+
rr: z.number().optional(),
|
|
138
|
+
limitPrice: z.number().optional(),
|
|
139
|
+
},
|
|
140
|
+
close_position: {
|
|
141
|
+
sessionId: z.string(),
|
|
142
|
+
symbol: z.string().optional(),
|
|
143
|
+
},
|
|
144
|
+
flatten: {
|
|
145
|
+
sessionId: z.string(),
|
|
146
|
+
},
|
|
147
|
+
cancel_order: {
|
|
148
|
+
sessionId: z.string(),
|
|
149
|
+
orderId: z.string(),
|
|
150
|
+
},
|
|
151
|
+
account: {
|
|
152
|
+
sessionId: z.string(),
|
|
153
|
+
},
|
|
154
|
+
positions: {
|
|
155
|
+
sessionId: z.string(),
|
|
156
|
+
},
|
|
157
|
+
recent_events: {
|
|
158
|
+
sessionId: z.string(),
|
|
159
|
+
limit: z.number().optional(),
|
|
160
|
+
},
|
|
161
|
+
attach_strategy: {
|
|
162
|
+
sessionId: z.string(),
|
|
163
|
+
strategy: z.string(),
|
|
164
|
+
params: z.record(z.string(), z.any()).optional(),
|
|
165
|
+
},
|
|
166
|
+
halt_all: {},
|
|
48
167
|
};
|
package/src/mcp/server.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import { mcpTools } from "./tools.js";
|
|
4
5
|
import { schemas } from "./schemas.js";
|
|
5
6
|
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = _require("../../package.json");
|
|
9
|
+
|
|
6
10
|
/** Build (but do not start) an McpServer with all tradelab tools registered. */
|
|
7
11
|
export function createServer() {
|
|
8
|
-
const server = new McpServer({ name: "tradelab", version
|
|
12
|
+
const server = new McpServer({ name: "tradelab", version });
|
|
9
13
|
|
|
10
14
|
for (const [name, def] of Object.entries(mcpTools)) {
|
|
11
15
|
server.tool(name, def.description, schemas[name] ?? {}, async (args) => {
|
package/src/mcp/tools.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { backtest } from "../engine/backtest.js";
|
|
2
2
|
import { walkForwardOptimize } from "../engine/walkForward.js";
|
|
3
|
-
import { getHistoricalCandles } from "../data/index.js";
|
|
3
|
+
import { getHistoricalCandles, candleStats } from "../data/index.js";
|
|
4
4
|
import { getStrategy, listStrategies } from "../strategies/index.js";
|
|
5
|
+
import { grid } from "../engine/grid.js";
|
|
6
|
+
import { monteCarlo, deflatedSharpe } from "../research/index.js";
|
|
7
|
+
import { liveTools } from "./liveTools.js";
|
|
5
8
|
|
|
6
9
|
function summarizeMetrics(metrics) {
|
|
7
10
|
const {
|
|
@@ -57,7 +60,7 @@ function expandGrid(grid) {
|
|
|
57
60
|
);
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
export const
|
|
63
|
+
export const researchTools = {
|
|
61
64
|
list_strategies: {
|
|
62
65
|
description: "List built-in trading strategies with their tunable parameters.",
|
|
63
66
|
handler: async () => ({ strategies: listStrategies() }),
|
|
@@ -140,3 +143,123 @@ export const mcpTools = {
|
|
|
140
143
|
},
|
|
141
144
|
},
|
|
142
145
|
};
|
|
146
|
+
|
|
147
|
+
export const researchPlusTools = {
|
|
148
|
+
analyze_robustness: {
|
|
149
|
+
description:
|
|
150
|
+
"Run a backtest on a named strategy then Monte Carlo + Deflated Sharpe on the realized trade PnLs. Degrades gracefully if fewer than 2 trades.",
|
|
151
|
+
handler: async (args) => {
|
|
152
|
+
const candles = await resolveCandles(args);
|
|
153
|
+
const factory = getStrategy(args.strategy);
|
|
154
|
+
const signal = factory(args.params || {});
|
|
155
|
+
const result = backtest({
|
|
156
|
+
candles,
|
|
157
|
+
symbol: args.symbol ?? "UNKNOWN",
|
|
158
|
+
interval: args.interval,
|
|
159
|
+
signal,
|
|
160
|
+
collectReplay: false,
|
|
161
|
+
warmupBars: 0,
|
|
162
|
+
...(args.backtestOptions || {}),
|
|
163
|
+
});
|
|
164
|
+
const metrics = summarizeMetrics(result.metrics);
|
|
165
|
+
const tradePnls = result.positions.map((p) => p.exit.pnl);
|
|
166
|
+
if (tradePnls.length < 2) {
|
|
167
|
+
return {
|
|
168
|
+
metrics,
|
|
169
|
+
monteCarlo: null,
|
|
170
|
+
deflatedSharpe: null,
|
|
171
|
+
note: `Only ${tradePnls.length} trade(s) — need at least 2 for statistical analysis.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const mc = monteCarlo({
|
|
175
|
+
tradePnls,
|
|
176
|
+
equityStart: args.equityStart ?? 10_000,
|
|
177
|
+
iterations: args.iterations ?? 1000,
|
|
178
|
+
blockSize: args.blockSize ?? 1,
|
|
179
|
+
seed: args.seed ?? "tradelab-mc",
|
|
180
|
+
});
|
|
181
|
+
const dsr = deflatedSharpe({
|
|
182
|
+
sharpe: result.metrics.sharpe,
|
|
183
|
+
sampleSize: result.metrics.trades,
|
|
184
|
+
numTrials: args.numTrials ?? 1,
|
|
185
|
+
sharpeStd: args.sharpeStd ?? 0,
|
|
186
|
+
skew: args.skew ?? 0,
|
|
187
|
+
kurtosis: args.kurtosis ?? 3,
|
|
188
|
+
});
|
|
189
|
+
return { metrics, monteCarlo: mc, deflatedSharpe: dsr };
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
optimize_strategy: {
|
|
194
|
+
description:
|
|
195
|
+
"In-process grid sweep of a named strategy. Returns a leaderboard ranked by a chosen metric (default: profitFactor).",
|
|
196
|
+
handler: async (args) => {
|
|
197
|
+
const candles = await resolveCandles(args);
|
|
198
|
+
const factory = getStrategy(args.strategy);
|
|
199
|
+
const scoreBy = args.scoreBy ?? "profitFactor";
|
|
200
|
+
const paramSets = grid(args.grid || {});
|
|
201
|
+
const rows = paramSets.map((params) => {
|
|
202
|
+
const signal = factory(params);
|
|
203
|
+
const result = backtest({
|
|
204
|
+
candles,
|
|
205
|
+
symbol: args.symbol ?? "UNKNOWN",
|
|
206
|
+
interval: args.interval,
|
|
207
|
+
signal,
|
|
208
|
+
collectReplay: false,
|
|
209
|
+
...(args.backtestOptions || {}),
|
|
210
|
+
});
|
|
211
|
+
const raw = result.metrics[scoreBy];
|
|
212
|
+
const score = Number.isFinite(raw) ? raw : -Infinity;
|
|
213
|
+
return { params, score, metrics: summarizeMetrics(result.metrics) };
|
|
214
|
+
});
|
|
215
|
+
rows.sort((a, b) => b.score - a.score);
|
|
216
|
+
return { leaderboard: rows, best: rows[0] ?? null };
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
compare_strategies: {
|
|
221
|
+
description:
|
|
222
|
+
"Run several named strategies on the same candle dataset and return a ranked comparison.",
|
|
223
|
+
handler: async (args) => {
|
|
224
|
+
const candles = await resolveCandles(args);
|
|
225
|
+
const scoreBy = args.scoreBy ?? "profitFactor";
|
|
226
|
+
const entries = args.strategies ?? [];
|
|
227
|
+
const rows = entries.map(({ strategy, params }) => {
|
|
228
|
+
const factory = getStrategy(strategy);
|
|
229
|
+
const signal = factory(params || {});
|
|
230
|
+
const result = backtest({
|
|
231
|
+
candles,
|
|
232
|
+
symbol: args.symbol ?? "UNKNOWN",
|
|
233
|
+
interval: args.interval,
|
|
234
|
+
signal,
|
|
235
|
+
collectReplay: false,
|
|
236
|
+
...(args.backtestOptions || {}),
|
|
237
|
+
});
|
|
238
|
+
const raw = result.metrics[scoreBy];
|
|
239
|
+
const score = Number.isFinite(raw) ? raw : -Infinity;
|
|
240
|
+
return { strategy, params: params ?? {}, score, metrics: summarizeMetrics(result.metrics) };
|
|
241
|
+
});
|
|
242
|
+
rows.sort((a, b) => b.score - a.score);
|
|
243
|
+
return { rankedBy: scoreBy, results: rows };
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
candle_stats: {
|
|
248
|
+
description:
|
|
249
|
+
"Return shape statistics (count, date range, price range, estimated interval) for an inline candle array or a data spec. Useful for sanity-checking data before backtesting.",
|
|
250
|
+
handler: async (args) => {
|
|
251
|
+
const candles = await resolveCandles(args);
|
|
252
|
+
const stats = candleStats(candles);
|
|
253
|
+
if (!stats) {
|
|
254
|
+
return { stats: null, note: "No candles returned." };
|
|
255
|
+
}
|
|
256
|
+
const gapNote =
|
|
257
|
+
stats.estimatedIntervalMin > 0
|
|
258
|
+
? `Estimated bar interval ~${stats.estimatedIntervalMin} min.`
|
|
259
|
+
: "Could not estimate interval.";
|
|
260
|
+
return { stats, note: gapNote };
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const mcpTools = { ...researchTools, ...researchPlusTools, ...liveTools };
|