tradelab 1.1.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 +57 -0
- package/README.md +183 -373
- package/dist/cjs/index.cjs +39 -12
- package/dist/cjs/live.cjs +457 -18
- 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 +6 -2
- package/src/live/dashboard/server.js +67 -8
- package/src/live/engine/paperEngine.js +21 -11
- package/src/live/index.js +2 -0
- package/src/live/session.js +439 -0
- package/src/mcp/liveTools.js +202 -0
- package/src/mcp/schemas.js +119 -0
- package/src/mcp/server.js +5 -1
- package/src/mcp/tools.js +125 -2
- package/src/research/monteCarlo.js +6 -2
- package/templates/dashboard.html +595 -108
- package/types/index.d.ts +25 -0
- package/types/live.d.ts +102 -1
- 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
|
@@ -1,758 +0,0 @@
|
|
|
1
|
-
# `tradelab/mcp` Server Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Ship an MCP server that exposes tradelab's research loop (fetch data → choose/parameterize a strategy → backtest → read metrics → walk-forward) as tools any agent (Claude Desktop, Cursor, Claude Code) can call autonomously.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Agents cannot pass JS closures over MCP, so strategies are **named and parameterized** through a registry (`src/strategies/`). The MCP tools are pure async functions in `src/mcp/tools.js` (unit-testable without a transport); `src/mcp/server.js` wires them to `@modelcontextprotocol/sdk`'s `McpServer` over stdio. Tool outputs are LLM-sized summaries (metrics + trade counts), never full replay frames. A `bin/tradelab-mcp.js` launches the server.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node ESM, `@modelcontextprotocol/sdk`, `zod` for tool input schemas, `node:test`. Builds on Plan 1 (clean metrics) and Plan 4 (`backtestAsync`, optional).
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: Named strategy registry
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
|
|
17
|
-
- Create: `src/strategies/index.js`
|
|
18
|
-
- Create: `src/strategies/builtins.js`
|
|
19
|
-
- Modify: `src/index.js`
|
|
20
|
-
- Test: `test/strategies/registry.test.js`
|
|
21
|
-
|
|
22
|
-
- [ ] **Step 1: Write the failing test**
|
|
23
|
-
|
|
24
|
-
```js
|
|
25
|
-
// test/strategies/registry.test.js
|
|
26
|
-
import test from "node:test";
|
|
27
|
-
import assert from "node:assert/strict";
|
|
28
|
-
import { listStrategies, getStrategy } from "../../src/strategies/index.js";
|
|
29
|
-
|
|
30
|
-
test("listStrategies returns built-ins with name, description, params", () => {
|
|
31
|
-
const all = listStrategies();
|
|
32
|
-
const names = all.map((s) => s.name);
|
|
33
|
-
assert.ok(names.includes("ema-cross"));
|
|
34
|
-
assert.ok(names.includes("rsi-reversion"));
|
|
35
|
-
assert.ok(names.includes("donchian-breakout"));
|
|
36
|
-
assert.ok(names.includes("buy-hold"));
|
|
37
|
-
const ema = all.find((s) => s.name === "ema-cross");
|
|
38
|
-
assert.equal(typeof ema.description, "string");
|
|
39
|
-
assert.equal(typeof ema.params.fast.default, "number");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("getStrategy returns a signalFactory producing a working signal", () => {
|
|
43
|
-
const factory = getStrategy("ema-cross");
|
|
44
|
-
const signal = factory({ fast: 3, slow: 5, rr: 2 });
|
|
45
|
-
assert.equal(typeof signal, "function");
|
|
46
|
-
const candles = Array.from({ length: 20 }, (_, i) => ({
|
|
47
|
-
time: i * 60000,
|
|
48
|
-
high: 101 + i,
|
|
49
|
-
low: 99 + i,
|
|
50
|
-
close: 100 + i,
|
|
51
|
-
}));
|
|
52
|
-
const out = signal({ candles, index: 19, bar: candles[19], equity: 10_000 });
|
|
53
|
-
assert.ok(out === null || typeof out === "object");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("getStrategy throws on unknown name with the available list", () => {
|
|
57
|
-
assert.throws(() => getStrategy("nope"), /Unknown strategy "nope"/);
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
- [ ] **Step 2: Run to verify it fails**
|
|
62
|
-
|
|
63
|
-
Run: `node --test test/strategies/registry.test.js`
|
|
64
|
-
Expected: FAIL — cannot find module.
|
|
65
|
-
|
|
66
|
-
- [ ] **Step 3: Implement src/strategies/builtins.js**
|
|
67
|
-
|
|
68
|
-
```js
|
|
69
|
-
// src/strategies/builtins.js
|
|
70
|
-
import { ema } from "../utils/indicators.js";
|
|
71
|
-
import { rsi } from "../ta/oscillators.js";
|
|
72
|
-
import { donchian } from "../ta/channels.js";
|
|
73
|
-
|
|
74
|
-
// Each entry: { description, params: { name: {type, default, description} }, factory }
|
|
75
|
-
// factory(params) => signal(context) compatible with backtest().
|
|
76
|
-
|
|
77
|
-
export const BUILTINS = {
|
|
78
|
-
"ema-cross": {
|
|
79
|
-
description: "Long when fast EMA crosses above slow EMA; stop at recent swing low.",
|
|
80
|
-
params: {
|
|
81
|
-
fast: { type: "number", default: 10, description: "fast EMA period" },
|
|
82
|
-
slow: { type: "number", default: 30, description: "slow EMA period" },
|
|
83
|
-
rr: { type: "number", default: 2, description: "reward:risk target" },
|
|
84
|
-
lookback: { type: "number", default: 15, description: "swing-low lookback for stop" },
|
|
85
|
-
},
|
|
86
|
-
factory({ fast = 10, slow = 30, rr = 2, lookback = 15 } = {}) {
|
|
87
|
-
return ({ candles, bar }) => {
|
|
88
|
-
if (candles.length < slow + 2) return null;
|
|
89
|
-
const closes = candles.map((c) => c.close);
|
|
90
|
-
const f = ema(closes, fast);
|
|
91
|
-
const s = ema(closes, slow);
|
|
92
|
-
const last = closes.length - 1;
|
|
93
|
-
if (f[last - 1] <= s[last - 1] && f[last] > s[last]) {
|
|
94
|
-
const stop = Math.min(...candles.slice(-lookback).map((c) => c.low));
|
|
95
|
-
if (stop >= bar.close) return null;
|
|
96
|
-
return { side: "long", entry: bar.close, stop, rr };
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
"rsi-reversion": {
|
|
104
|
-
description: "Long when RSI dips below `oversold`; stop a fixed pct below entry.",
|
|
105
|
-
params: {
|
|
106
|
-
period: { type: "number", default: 14, description: "RSI period" },
|
|
107
|
-
oversold: { type: "number", default: 30, description: "RSI entry threshold" },
|
|
108
|
-
stopPct: { type: "number", default: 2, description: "stop distance in percent" },
|
|
109
|
-
rr: { type: "number", default: 1.5, description: "reward:risk target" },
|
|
110
|
-
},
|
|
111
|
-
factory({ period = 14, oversold = 30, stopPct = 2, rr = 1.5 } = {}) {
|
|
112
|
-
return ({ candles, bar }) => {
|
|
113
|
-
if (candles.length < period + 2) return null;
|
|
114
|
-
const values = rsi(
|
|
115
|
-
candles.map((c) => c.close),
|
|
116
|
-
period
|
|
117
|
-
);
|
|
118
|
-
const r = values[values.length - 1];
|
|
119
|
-
if (r === undefined || r > oversold) return null;
|
|
120
|
-
return { side: "long", entry: bar.close, stop: bar.close * (1 - stopPct / 100), rr };
|
|
121
|
-
};
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
"donchian-breakout": {
|
|
126
|
-
description: "Long on a close above the prior Donchian upper channel.",
|
|
127
|
-
params: {
|
|
128
|
-
period: { type: "number", default: 20, description: "channel lookback" },
|
|
129
|
-
rr: { type: "number", default: 2, description: "reward:risk target" },
|
|
130
|
-
},
|
|
131
|
-
factory({ period = 20, rr = 2 } = {}) {
|
|
132
|
-
return ({ candles, bar }) => {
|
|
133
|
-
if (candles.length < period + 2) return null;
|
|
134
|
-
const ch = donchian(candles, period);
|
|
135
|
-
const i = candles.length - 1;
|
|
136
|
-
const priorUpper = ch.upper[i - 1];
|
|
137
|
-
const priorLower = ch.lower[i - 1];
|
|
138
|
-
if (priorUpper === undefined) return null;
|
|
139
|
-
if (bar.close > priorUpper) {
|
|
140
|
-
return { side: "long", entry: bar.close, stop: priorLower, rr };
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
};
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
"buy-hold": {
|
|
148
|
-
description: "Enter once at the first eligible bar and hold for `holdBars`.",
|
|
149
|
-
params: {
|
|
150
|
-
holdBars: { type: "number", default: 5, description: "bars to hold before exit" },
|
|
151
|
-
stopPct: { type: "number", default: 10, description: "protective stop distance in percent" },
|
|
152
|
-
},
|
|
153
|
-
factory({ holdBars = 5, stopPct = 10 } = {}) {
|
|
154
|
-
let entered = false;
|
|
155
|
-
return ({ bar }) => {
|
|
156
|
-
if (entered) return null;
|
|
157
|
-
entered = true;
|
|
158
|
-
return {
|
|
159
|
-
side: "long",
|
|
160
|
-
entry: bar.close,
|
|
161
|
-
stop: bar.close * (1 - stopPct / 100),
|
|
162
|
-
rr: 5,
|
|
163
|
-
_maxBarsInTrade: holdBars,
|
|
164
|
-
};
|
|
165
|
-
};
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
- [ ] **Step 4: Implement src/strategies/index.js**
|
|
172
|
-
|
|
173
|
-
```js
|
|
174
|
-
// src/strategies/index.js
|
|
175
|
-
import { BUILTINS } from "./builtins.js";
|
|
176
|
-
|
|
177
|
-
const registry = new Map(Object.entries(BUILTINS));
|
|
178
|
-
|
|
179
|
-
/** Register a custom strategy at runtime. `def` is a BUILTINS-shaped object. */
|
|
180
|
-
export function registerStrategy(name, def) {
|
|
181
|
-
if (typeof def?.factory !== "function") {
|
|
182
|
-
throw new Error(`registerStrategy("${name}") requires a factory function`);
|
|
183
|
-
}
|
|
184
|
-
registry.set(name, def);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** List all strategies as { name, description, params }. */
|
|
188
|
-
export function listStrategies() {
|
|
189
|
-
return [...registry.entries()].map(([name, def]) => ({
|
|
190
|
-
name,
|
|
191
|
-
description: def.description,
|
|
192
|
-
params: def.params,
|
|
193
|
-
}));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Get a strategy's signalFactory(params) => signal. Throws on unknown name. */
|
|
197
|
-
export function getStrategy(name) {
|
|
198
|
-
const def = registry.get(name);
|
|
199
|
-
if (!def) {
|
|
200
|
-
const available = [...registry.keys()].join(", ");
|
|
201
|
-
throw new Error(`Unknown strategy "${name}". Available: ${available}`);
|
|
202
|
-
}
|
|
203
|
-
return def.factory;
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
- [ ] **Step 5: Export from src/index.js**
|
|
208
|
-
|
|
209
|
-
```js
|
|
210
|
-
export { listStrategies, getStrategy, registerStrategy } from "./strategies/index.js";
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
- [ ] **Step 6: Run test + suite**
|
|
214
|
-
|
|
215
|
-
Run: `node --test test/strategies/registry.test.js`
|
|
216
|
-
Expected: PASS (3 tests).
|
|
217
|
-
|
|
218
|
-
Run: `node --test`
|
|
219
|
-
Expected: PASS.
|
|
220
|
-
|
|
221
|
-
- [ ] **Step 7: Commit**
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
git add src/strategies/index.js src/strategies/builtins.js src/index.js test/strategies/registry.test.js
|
|
225
|
-
git commit -m "feat: add named strategy registry with built-ins
|
|
226
|
-
|
|
227
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
### Task 2: MCP tool functions (transport-free, testable)
|
|
233
|
-
|
|
234
|
-
**Files:**
|
|
235
|
-
|
|
236
|
-
- Create: `src/mcp/tools.js`
|
|
237
|
-
- Test: `test/mcp/tools.test.js`
|
|
238
|
-
|
|
239
|
-
- [ ] **Step 1: Write the failing test**
|
|
240
|
-
|
|
241
|
-
```js
|
|
242
|
-
// test/mcp/tools.test.js
|
|
243
|
-
import test from "node:test";
|
|
244
|
-
import assert from "node:assert/strict";
|
|
245
|
-
import { mcpTools } from "../../src/mcp/tools.js";
|
|
246
|
-
|
|
247
|
-
function buildCandles(n = 60) {
|
|
248
|
-
const start = Date.UTC(2025, 0, 2, 14, 30, 0);
|
|
249
|
-
return Array.from({ length: n }, (_, i) => ({
|
|
250
|
-
time: start + i * 86_400_000,
|
|
251
|
-
open: 100 + i,
|
|
252
|
-
high: 101 + i,
|
|
253
|
-
low: 99 + i,
|
|
254
|
-
close: 100 + i + (i % 3 === 0 ? -0.5 : 0.5),
|
|
255
|
-
volume: 1000 + i,
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
test("list_strategies returns the registry", async () => {
|
|
260
|
-
const out = await mcpTools.list_strategies.handler({});
|
|
261
|
-
assert.ok(Array.isArray(out.strategies));
|
|
262
|
-
assert.ok(out.strategies.some((s) => s.name === "ema-cross"));
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("run_backtest with inline candles returns an LLM-sized metrics summary", async () => {
|
|
266
|
-
const candles = buildCandles();
|
|
267
|
-
const out = await mcpTools.run_backtest.handler({
|
|
268
|
-
candles,
|
|
269
|
-
symbol: "TEST",
|
|
270
|
-
interval: "1d",
|
|
271
|
-
strategy: "ema-cross",
|
|
272
|
-
params: { fast: 3, slow: 5, rr: 2 },
|
|
273
|
-
});
|
|
274
|
-
assert.equal(typeof out.metrics.trades, "number");
|
|
275
|
-
assert.equal(typeof out.metrics.profitFactor, "number");
|
|
276
|
-
assert.equal("sharpeAnnualized" in out.metrics, true);
|
|
277
|
-
assert.equal("replay" in out, false); // never ship replay to an agent
|
|
278
|
-
assert.ok(out.tradesPreview.length <= 10);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("run_backtest rejects an unknown strategy", async () => {
|
|
282
|
-
await assert.rejects(() =>
|
|
283
|
-
mcpTools.run_backtest.handler({ candles: buildCandles(), strategy: "ghost" })
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("walk_forward runs a parameter grid and returns window stability", async () => {
|
|
288
|
-
const candles = buildCandles(200);
|
|
289
|
-
const out = await mcpTools.walk_forward.handler({
|
|
290
|
-
candles,
|
|
291
|
-
interval: "1d",
|
|
292
|
-
strategy: "ema-cross",
|
|
293
|
-
trainBars: 60,
|
|
294
|
-
testBars: 20,
|
|
295
|
-
mode: "anchored",
|
|
296
|
-
grid: { fast: [3, 5], slow: [8, 13] },
|
|
297
|
-
});
|
|
298
|
-
assert.ok(out.windows >= 1);
|
|
299
|
-
assert.equal(typeof out.metrics.totalPnL, "number");
|
|
300
|
-
assert.equal(typeof out.stability.uniqueWinnerCount, "number");
|
|
301
|
-
});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
- [ ] **Step 2: Run to verify it fails**
|
|
305
|
-
|
|
306
|
-
Run: `node --test test/mcp/tools.test.js`
|
|
307
|
-
Expected: FAIL — cannot find module.
|
|
308
|
-
|
|
309
|
-
- [ ] **Step 3: Implement src/mcp/tools.js**
|
|
310
|
-
|
|
311
|
-
```js
|
|
312
|
-
// src/mcp/tools.js
|
|
313
|
-
import { backtest } from "../engine/backtest.js";
|
|
314
|
-
import { walkForwardOptimize } from "../engine/walkForward.js";
|
|
315
|
-
import { getHistoricalCandles } from "../data/index.js";
|
|
316
|
-
import { getStrategy, listStrategies } from "../strategies/index.js";
|
|
317
|
-
|
|
318
|
-
// Strip heavy fields so tool output stays within an agent's context budget.
|
|
319
|
-
function summarizeMetrics(metrics) {
|
|
320
|
-
const {
|
|
321
|
-
trades,
|
|
322
|
-
winRate,
|
|
323
|
-
profitFactor,
|
|
324
|
-
expectancy,
|
|
325
|
-
totalR,
|
|
326
|
-
avgR,
|
|
327
|
-
sharpe,
|
|
328
|
-
sharpeAnnualized,
|
|
329
|
-
sortinoAnnualized,
|
|
330
|
-
maxDrawdown,
|
|
331
|
-
calmar,
|
|
332
|
-
returnPct,
|
|
333
|
-
totalPnL,
|
|
334
|
-
finalEquity,
|
|
335
|
-
exposurePct,
|
|
336
|
-
sideBreakdown,
|
|
337
|
-
} = metrics;
|
|
338
|
-
return {
|
|
339
|
-
trades,
|
|
340
|
-
winRate,
|
|
341
|
-
profitFactor,
|
|
342
|
-
expectancy,
|
|
343
|
-
totalR,
|
|
344
|
-
avgR,
|
|
345
|
-
sharpe,
|
|
346
|
-
sharpeAnnualized,
|
|
347
|
-
sortinoAnnualized,
|
|
348
|
-
maxDrawdown,
|
|
349
|
-
calmar,
|
|
350
|
-
returnPct,
|
|
351
|
-
totalPnL,
|
|
352
|
-
finalEquity,
|
|
353
|
-
exposurePct,
|
|
354
|
-
sideBreakdown,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function resolveCandles(args) {
|
|
359
|
-
if (Array.isArray(args.candles) && args.candles.length) return args.candles;
|
|
360
|
-
if (args.data) return getHistoricalCandles(args.data);
|
|
361
|
-
throw new Error("Provide either `candles` (array) or `data` (getHistoricalCandles spec).");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Cartesian product of a { key: value[] } grid into parameter set objects.
|
|
365
|
-
function expandGrid(grid) {
|
|
366
|
-
const keys = Object.keys(grid || {});
|
|
367
|
-
if (!keys.length) return [{}];
|
|
368
|
-
return keys.reduce(
|
|
369
|
-
(acc, key) => acc.flatMap((base) => grid[key].map((v) => ({ ...base, [key]: v }))),
|
|
370
|
-
[{}]
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export const mcpTools = {
|
|
375
|
-
list_strategies: {
|
|
376
|
-
description: "List built-in trading strategies with their tunable parameters.",
|
|
377
|
-
handler: async () => ({ strategies: listStrategies() }),
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
fetch_candles: {
|
|
381
|
-
description: "Download/caches OHLCV candles from Yahoo or CSV. Returns a compact summary.",
|
|
382
|
-
handler: async (args) => {
|
|
383
|
-
const candles = await getHistoricalCandles(args);
|
|
384
|
-
return {
|
|
385
|
-
count: candles.length,
|
|
386
|
-
first: candles[0] ?? null,
|
|
387
|
-
last: candles[candles.length - 1] ?? null,
|
|
388
|
-
};
|
|
389
|
-
},
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
run_backtest: {
|
|
393
|
-
description:
|
|
394
|
-
"Run a single backtest using a named strategy + params. Returns a metrics summary and a small trade preview (no replay).",
|
|
395
|
-
handler: async (args) => {
|
|
396
|
-
const candles = await resolveCandles(args);
|
|
397
|
-
const factory = getStrategy(args.strategy);
|
|
398
|
-
const signal = factory(args.params || {});
|
|
399
|
-
const result = backtest({
|
|
400
|
-
candles,
|
|
401
|
-
symbol: args.symbol ?? "UNKNOWN",
|
|
402
|
-
interval: args.interval,
|
|
403
|
-
signal,
|
|
404
|
-
collectReplay: false,
|
|
405
|
-
...(args.backtestOptions || {}),
|
|
406
|
-
});
|
|
407
|
-
return {
|
|
408
|
-
symbol: result.symbol,
|
|
409
|
-
interval: result.interval,
|
|
410
|
-
metrics: summarizeMetrics(result.metrics),
|
|
411
|
-
tradesPreview: result.positions.slice(0, 10).map((p) => ({
|
|
412
|
-
side: p.side,
|
|
413
|
-
entry: p.entryFill ?? p.entry,
|
|
414
|
-
exit: p.exit.price,
|
|
415
|
-
pnl: p.exit.pnl,
|
|
416
|
-
reason: p.exit.reason,
|
|
417
|
-
})),
|
|
418
|
-
};
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
|
|
422
|
-
walk_forward: {
|
|
423
|
-
description:
|
|
424
|
-
"Walk-forward optimize a named strategy over a parameter grid. Returns out-of-sample metrics and winner stability.",
|
|
425
|
-
handler: async (args) => {
|
|
426
|
-
const candles = await resolveCandles(args);
|
|
427
|
-
const factory = getStrategy(args.strategy);
|
|
428
|
-
const wf = walkForwardOptimize({
|
|
429
|
-
candles,
|
|
430
|
-
mode: args.mode ?? "rolling",
|
|
431
|
-
trainBars: args.trainBars,
|
|
432
|
-
testBars: args.testBars,
|
|
433
|
-
stepBars: args.stepBars ?? args.testBars,
|
|
434
|
-
scoreBy: args.scoreBy ?? "profitFactor",
|
|
435
|
-
parameterSets: expandGrid(args.grid),
|
|
436
|
-
signalFactory: (params) => factory(params),
|
|
437
|
-
backtestOptions: {
|
|
438
|
-
interval: args.interval,
|
|
439
|
-
collectReplay: false,
|
|
440
|
-
...(args.backtestOptions || {}),
|
|
441
|
-
},
|
|
442
|
-
});
|
|
443
|
-
return {
|
|
444
|
-
windows: wf.windows.length,
|
|
445
|
-
metrics: summarizeMetrics(wf.metrics),
|
|
446
|
-
stability: wf.bestParamsSummary,
|
|
447
|
-
windowSummaries: wf.windows.map((w) => ({
|
|
448
|
-
bestParams: w.bestParams,
|
|
449
|
-
oosTrades: w.oosTrades,
|
|
450
|
-
profitable: w.profitable,
|
|
451
|
-
stabilityScore: w.stabilityScore,
|
|
452
|
-
})),
|
|
453
|
-
};
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
};
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
- [ ] **Step 4: Run test + suite**
|
|
460
|
-
|
|
461
|
-
Run: `node --test test/mcp/tools.test.js`
|
|
462
|
-
Expected: PASS (4 tests).
|
|
463
|
-
|
|
464
|
-
Run: `node --test`
|
|
465
|
-
Expected: PASS.
|
|
466
|
-
|
|
467
|
-
- [ ] **Step 5: Commit**
|
|
468
|
-
|
|
469
|
-
```bash
|
|
470
|
-
git add src/mcp/tools.js test/mcp/tools.test.js
|
|
471
|
-
git commit -m "feat: add transport-free MCP tool functions
|
|
472
|
-
|
|
473
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
---
|
|
477
|
-
|
|
478
|
-
### Task 3: Add MCP SDK dependencies + subpath
|
|
479
|
-
|
|
480
|
-
**Files:**
|
|
481
|
-
|
|
482
|
-
- Modify: `package.json`
|
|
483
|
-
|
|
484
|
-
- [ ] **Step 1: Install the SDK and zod**
|
|
485
|
-
|
|
486
|
-
Run:
|
|
487
|
-
|
|
488
|
-
```bash
|
|
489
|
-
npm install @modelcontextprotocol/sdk zod
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
Expected: both added to `dependencies` in `package.json`, lockfile updated.
|
|
493
|
-
|
|
494
|
-
- [ ] **Step 2: Add the `./mcp` export and `tradelab-mcp` bin**
|
|
495
|
-
|
|
496
|
-
In `package.json`, in the `exports` map add:
|
|
497
|
-
|
|
498
|
-
```json
|
|
499
|
-
"./mcp": {
|
|
500
|
-
"import": "./src/mcp/server.js"
|
|
501
|
-
},
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
In the `bin` map add:
|
|
505
|
-
|
|
506
|
-
```json
|
|
507
|
-
"tradelab-mcp": "bin/tradelab-mcp.js"
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Add `"bin"` is already listed in `files`. Confirm `bin` stays in the `files` array.
|
|
511
|
-
|
|
512
|
-
- [ ] **Step 3: Commit**
|
|
513
|
-
|
|
514
|
-
```bash
|
|
515
|
-
git add package.json package-lock.json
|
|
516
|
-
git commit -m "build: add MCP SDK + zod deps and tradelab-mcp bin
|
|
517
|
-
|
|
518
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
---
|
|
522
|
-
|
|
523
|
-
### Task 4: Wire tools into an McpServer over stdio
|
|
524
|
-
|
|
525
|
-
**Files:**
|
|
526
|
-
|
|
527
|
-
- Create: `src/mcp/schemas.js` (zod input shapes)
|
|
528
|
-
- Create: `src/mcp/server.js`
|
|
529
|
-
- Create: `bin/tradelab-mcp.js`
|
|
530
|
-
- Test: `test/mcp/server.test.js`
|
|
531
|
-
|
|
532
|
-
- [ ] **Step 1: Write the failing test**
|
|
533
|
-
|
|
534
|
-
```js
|
|
535
|
-
// test/mcp/server.test.js
|
|
536
|
-
import test from "node:test";
|
|
537
|
-
import assert from "node:assert/strict";
|
|
538
|
-
import { createServer } from "../../src/mcp/server.js";
|
|
539
|
-
|
|
540
|
-
test("createServer registers all tradelab tools", () => {
|
|
541
|
-
const server = createServer();
|
|
542
|
-
// McpServer exposes registered tool names via its internal registry; we assert
|
|
543
|
-
// the factory returns an object with a connect method (the SDK server handle).
|
|
544
|
-
assert.equal(typeof server.connect, "function");
|
|
545
|
-
});
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
- [ ] **Step 2: Run to verify it fails**
|
|
549
|
-
|
|
550
|
-
Run: `node --test test/mcp/server.test.js`
|
|
551
|
-
Expected: FAIL — cannot find module.
|
|
552
|
-
|
|
553
|
-
- [ ] **Step 3: Implement src/mcp/schemas.js**
|
|
554
|
-
|
|
555
|
-
```js
|
|
556
|
-
// src/mcp/schemas.js
|
|
557
|
-
import { z } from "zod";
|
|
558
|
-
|
|
559
|
-
const candle = z.object({
|
|
560
|
-
time: z.number(),
|
|
561
|
-
open: z.number().optional(),
|
|
562
|
-
high: z.number(),
|
|
563
|
-
low: z.number(),
|
|
564
|
-
close: z.number(),
|
|
565
|
-
volume: z.number().optional(),
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const dataSpec = z
|
|
569
|
-
.object({
|
|
570
|
-
source: z.enum(["yahoo", "csv", "auto"]).optional(),
|
|
571
|
-
symbol: z.string().optional(),
|
|
572
|
-
interval: z.string().optional(),
|
|
573
|
-
period: z.string().optional(),
|
|
574
|
-
csvPath: z.string().optional(),
|
|
575
|
-
cache: z.boolean().optional(),
|
|
576
|
-
})
|
|
577
|
-
.passthrough();
|
|
578
|
-
|
|
579
|
-
export const schemas = {
|
|
580
|
-
list_strategies: {},
|
|
581
|
-
fetch_candles: dataSpec.shape,
|
|
582
|
-
run_backtest: {
|
|
583
|
-
candles: z.array(candle).optional(),
|
|
584
|
-
data: dataSpec.optional(),
|
|
585
|
-
symbol: z.string().optional(),
|
|
586
|
-
interval: z.string().optional(),
|
|
587
|
-
strategy: z.string(),
|
|
588
|
-
params: z.record(z.any()).optional(),
|
|
589
|
-
backtestOptions: z.record(z.any()).optional(),
|
|
590
|
-
},
|
|
591
|
-
walk_forward: {
|
|
592
|
-
candles: z.array(candle).optional(),
|
|
593
|
-
data: dataSpec.optional(),
|
|
594
|
-
interval: z.string().optional(),
|
|
595
|
-
strategy: z.string(),
|
|
596
|
-
trainBars: z.number(),
|
|
597
|
-
testBars: z.number(),
|
|
598
|
-
stepBars: z.number().optional(),
|
|
599
|
-
mode: z.enum(["rolling", "anchored"]).optional(),
|
|
600
|
-
scoreBy: z.string().optional(),
|
|
601
|
-
grid: z.record(z.array(z.any())).optional(),
|
|
602
|
-
backtestOptions: z.record(z.any()).optional(),
|
|
603
|
-
},
|
|
604
|
-
};
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
- [ ] **Step 4: Implement src/mcp/server.js**
|
|
608
|
-
|
|
609
|
-
```js
|
|
610
|
-
// src/mcp/server.js
|
|
611
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
612
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
613
|
-
import { mcpTools } from "./tools.js";
|
|
614
|
-
import { schemas } from "./schemas.js";
|
|
615
|
-
|
|
616
|
-
/** Build (but do not start) an McpServer with all tradelab tools registered. */
|
|
617
|
-
export function createServer() {
|
|
618
|
-
const server = new McpServer({ name: "tradelab", version: "1.1.0" });
|
|
619
|
-
|
|
620
|
-
for (const [name, def] of Object.entries(mcpTools)) {
|
|
621
|
-
server.tool(name, def.description, schemas[name] ?? {}, async (args) => {
|
|
622
|
-
try {
|
|
623
|
-
const result = await def.handler(args ?? {});
|
|
624
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
625
|
-
} catch (error) {
|
|
626
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
627
|
-
return { isError: true, content: [{ type: "text", text: `Error: ${message}` }] };
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return server;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/** Start the server on stdio. Called by bin/tradelab-mcp.js. */
|
|
636
|
-
export async function startStdioServer() {
|
|
637
|
-
const server = createServer();
|
|
638
|
-
const transport = new StdioServerTransport();
|
|
639
|
-
await server.connect(transport);
|
|
640
|
-
return server;
|
|
641
|
-
}
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
- [ ] **Step 5: Implement bin/tradelab-mcp.js**
|
|
645
|
-
|
|
646
|
-
```js
|
|
647
|
-
#!/usr/bin/env node
|
|
648
|
-
// bin/tradelab-mcp.js
|
|
649
|
-
import { startStdioServer } from "../src/mcp/server.js";
|
|
650
|
-
|
|
651
|
-
startStdioServer().catch((error) => {
|
|
652
|
-
console.error("tradelab-mcp failed to start:", error);
|
|
653
|
-
process.exit(1);
|
|
654
|
-
});
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
- [ ] **Step 6: Make the bin executable**
|
|
658
|
-
|
|
659
|
-
Run:
|
|
660
|
-
|
|
661
|
-
```bash
|
|
662
|
-
chmod +x bin/tradelab-mcp.js
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
- [ ] **Step 7: Run test + suite**
|
|
666
|
-
|
|
667
|
-
Run: `node --test test/mcp/server.test.js`
|
|
668
|
-
Expected: PASS.
|
|
669
|
-
|
|
670
|
-
Run: `node --test`
|
|
671
|
-
Expected: PASS.
|
|
672
|
-
|
|
673
|
-
- [ ] **Step 8: Smoke-test the server starts and lists tools**
|
|
674
|
-
|
|
675
|
-
Run (sends an MCP `initialize` + `tools/list` and exits):
|
|
676
|
-
|
|
677
|
-
```bash
|
|
678
|
-
node bin/tradelab-mcp.js < /dev/null & sleep 1; kill %1 2>/dev/null; echo "started ok"
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
Expected: process starts without throwing (it waits on stdio). For a real
|
|
682
|
-
handshake test, configure it in Claude Desktop (Task 5) — automated stdio
|
|
683
|
-
handshake testing is out of scope for unit tests.
|
|
684
|
-
|
|
685
|
-
- [ ] **Step 9: Commit**
|
|
686
|
-
|
|
687
|
-
```bash
|
|
688
|
-
git add src/mcp/schemas.js src/mcp/server.js bin/tradelab-mcp.js test/mcp/server.test.js
|
|
689
|
-
git commit -m "feat: MCP stdio server wiring tradelab tools
|
|
690
|
-
|
|
691
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
---
|
|
695
|
-
|
|
696
|
-
### Task 5: Docs — agent setup guide
|
|
697
|
-
|
|
698
|
-
**Files:**
|
|
699
|
-
|
|
700
|
-
- Create: `docs/mcp.md`
|
|
701
|
-
- Modify: `README.md`
|
|
702
|
-
|
|
703
|
-
- [ ] **Step 1: Write docs/mcp.md**
|
|
704
|
-
|
|
705
|
-
Document:
|
|
706
|
-
|
|
707
|
-
- What the server exposes (`list_strategies`, `fetch_candles`, `run_backtest`, `walk_forward`).
|
|
708
|
-
- The "agent research loop": list strategies → fetch candles → run_backtest → read metrics → walk_forward to validate.
|
|
709
|
-
- Claude Desktop config snippet:
|
|
710
|
-
|
|
711
|
-
````markdown
|
|
712
|
-
```json
|
|
713
|
-
{
|
|
714
|
-
"mcpServers": {
|
|
715
|
-
"tradelab": {
|
|
716
|
-
"command": "npx",
|
|
717
|
-
"args": ["-y", "tradelab", "tradelab-mcp"]
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
```
|
|
722
|
-
````
|
|
723
|
-
|
|
724
|
-
(After `npm install -g tradelab` you can instead use `"command": "tradelab-mcp"`.)
|
|
725
|
-
|
|
726
|
-
- A note that strategies are name-addressable (agents can't pass code), and how to
|
|
727
|
-
add custom strategies via `registerStrategy`.
|
|
728
|
-
|
|
729
|
-
- [ ] **Step 2: Link from README**
|
|
730
|
-
|
|
731
|
-
Add a documentation-table row:
|
|
732
|
-
|
|
733
|
-
```markdown
|
|
734
|
-
| [MCP server](docs/mcp.md) | Run the research loop from any MCP-capable agent (Claude, Cursor) |
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
And add a short "## AI agents / MCP" section near the top pointing to `docs/mcp.md`.
|
|
738
|
-
|
|
739
|
-
- [ ] **Step 3: Build, lint, test, commit**
|
|
740
|
-
|
|
741
|
-
```bash
|
|
742
|
-
npm run build && npm run lint && npm run format:check && npm test
|
|
743
|
-
git add docs/mcp.md README.md
|
|
744
|
-
git commit -m "docs: MCP server setup and agent research loop
|
|
745
|
-
|
|
746
|
-
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
---
|
|
750
|
-
|
|
751
|
-
## Self-review checklist
|
|
752
|
-
|
|
753
|
-
- [ ] Agents never need to pass code — every tool is name + JSON params. ✔ (Task 1, 2)
|
|
754
|
-
- [ ] Tool outputs are summaries; `replay` is explicitly excluded (asserted in test). ✔ (Task 2)
|
|
755
|
-
- [ ] `mcpTools` handlers are pure async functions tested without a transport; `server.js` is a thin SDK adapter. ✔
|
|
756
|
-
- [ ] Strategy names are consistent across registry, tools, tests, and docs (`ema-cross`, `rsi-reversion`, `donchian-breakout`, `buy-hold`). ✔
|
|
757
|
-
- [ ] `run_backtest` surfaces `sharpeAnnualized` (depends on Plan 1 being merged; if Plan 1 is not yet done, the field is simply absent — the summary still works). ✔
|
|
758
|
-
- [ ] New deps (`@modelcontextprotocol/sdk`, `zod`) are runtime `dependencies`, not `devDependencies`. ✔ (Task 3)
|