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.
Files changed (39) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +183 -373
  3. package/dist/cjs/index.cjs +39 -12
  4. package/dist/cjs/live.cjs +457 -18
  5. package/docs/README.md +32 -66
  6. package/docs/api-reference.md +269 -144
  7. package/docs/backtest-engine.md +167 -321
  8. package/docs/data-reporting-cli.md +114 -156
  9. package/docs/examples.md +6 -6
  10. package/docs/live-trading.md +254 -134
  11. package/docs/mcp.md +244 -23
  12. package/docs/research.md +99 -45
  13. package/examples/mcpLiveTrading.js +77 -0
  14. package/package.json +11 -3
  15. package/src/engine/optimize.js +25 -1
  16. package/src/engine/portfolio.js +6 -2
  17. package/src/live/dashboard/server.js +67 -8
  18. package/src/live/engine/paperEngine.js +21 -11
  19. package/src/live/index.js +2 -0
  20. package/src/live/session.js +439 -0
  21. package/src/mcp/liveTools.js +202 -0
  22. package/src/mcp/schemas.js +119 -0
  23. package/src/mcp/server.js +5 -1
  24. package/src/mcp/tools.js +125 -2
  25. package/src/research/monteCarlo.js +6 -2
  26. package/templates/dashboard.html +595 -108
  27. package/types/index.d.ts +25 -0
  28. package/types/live.d.ts +102 -1
  29. package/types/mcp.d.ts +17 -0
  30. package/docs/superpowers/plans/2026-00-overview.md +0 -101
  31. package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
  32. package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
  33. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
  34. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
  35. package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
  36. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
  37. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
  38. package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
  39. package/docs/superpowers/plans/HANDOFF.md +0 -88
@@ -0,0 +1,202 @@
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
+ 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
+
40
+ export { manager as sessionManager };
41
+
42
+ export const liveTools = {
43
+ create_session: {
44
+ description:
45
+ "Create a new paper (default) or live (gated) trading session. Paper needs no credentials.",
46
+ handler: async ({
47
+ sessionId,
48
+ symbol,
49
+ mode = "paper",
50
+ interval = "1m",
51
+ equity = 10_000,
52
+ riskPct,
53
+ maxDailyLossPct,
54
+ confirmLive = false,
55
+ } = {}) => {
56
+ const session = await manager.create({
57
+ id: sessionId,
58
+ symbol,
59
+ mode,
60
+ interval,
61
+ equity,
62
+ ...(riskPct != null ? { riskPct } : {}),
63
+ ...(maxDailyLossPct != null ? { maxDailyLossPct } : {}),
64
+ confirmLive,
65
+ });
66
+ return session.getStatus();
67
+ },
68
+ },
69
+
70
+ list_sessions: {
71
+ description: "List all active trading sessions and their current status.",
72
+ handler: async () => {
73
+ return manager.list().map((s) => s.getStatus());
74
+ },
75
+ },
76
+
77
+ session_status: {
78
+ description:
79
+ "Get a full refreshed status snapshot for a session (positions, orders, equity, risk).",
80
+ handler: async ({ sessionId } = {}) => {
81
+ const session = requireSession(sessionId);
82
+ return session.refresh();
83
+ },
84
+ },
85
+
86
+ feed_price: {
87
+ description:
88
+ "Feed a price bar (or single price) to a session, advancing paper simulations and triggering fills.",
89
+ handler: async ({ sessionId, bar, price } = {}) => {
90
+ const session = requireSession(sessionId);
91
+ let b = bar;
92
+ if (!b && Number.isFinite(price)) {
93
+ b = { time: Date.now(), open: price, high: price, low: price, close: price, volume: 0 };
94
+ }
95
+ if (!b) throw new Error("Provide either `bar` (OHLCV) or `price` (number)");
96
+ await session.pushBar(b);
97
+
98
+ // If a strategy is attached and session is flat, evaluate it
99
+ if (session._strategy && session.getStatus().positions.length === 0) {
100
+ try {
101
+ const signal = session._strategy(strategyContext(session));
102
+ if (signal && (signal.side || signal.direction || signal.action)) {
103
+ await session.placeOrder(signalToOrder(signal)).catch(() => {});
104
+ }
105
+ } catch {
106
+ // strategy errors are non-fatal
107
+ }
108
+ }
109
+
110
+ return session.getStatus();
111
+ },
112
+ },
113
+
114
+ place_order: {
115
+ description:
116
+ "Place a market or limit order in a session (optionally risk-sized with bracket stop/target).",
117
+ handler: async ({
118
+ sessionId,
119
+ side,
120
+ type = "market",
121
+ qty,
122
+ riskPct,
123
+ stop,
124
+ target,
125
+ rr,
126
+ limitPrice,
127
+ } = {}) => {
128
+ const session = requireSession(sessionId);
129
+ return session.placeOrder({ side, type, qty, riskPct, stop, target, rr, limitPrice });
130
+ },
131
+ },
132
+
133
+ close_position: {
134
+ description: "Close the open position for a symbol in a session via an opposite market order.",
135
+ handler: async ({ sessionId, symbol } = {}) => {
136
+ const session = requireSession(sessionId);
137
+ return session.closePosition(symbol);
138
+ },
139
+ },
140
+
141
+ flatten: {
142
+ description: "Flatten all positions and cancel all open orders in a session.",
143
+ handler: async ({ sessionId } = {}) => {
144
+ const session = requireSession(sessionId);
145
+ await session.flatten();
146
+ return { ok: true };
147
+ },
148
+ },
149
+
150
+ cancel_order: {
151
+ description: "Cancel a specific open order in a session.",
152
+ handler: async ({ sessionId, orderId } = {}) => {
153
+ const session = requireSession(sessionId);
154
+ await session.cancelOrder(orderId);
155
+ return { ok: true };
156
+ },
157
+ },
158
+
159
+ account: {
160
+ description: "Get the broker account details for a session (equity, cash, buying power).",
161
+ handler: async ({ sessionId } = {}) => {
162
+ const session = requireSession(sessionId);
163
+ return session.getAccount();
164
+ },
165
+ },
166
+
167
+ positions: {
168
+ description: "Get all open positions for a session.",
169
+ handler: async ({ sessionId } = {}) => {
170
+ const session = requireSession(sessionId);
171
+ return session.getPositions();
172
+ },
173
+ },
174
+
175
+ recent_events: {
176
+ description: "Get recent session events (fills, risk changes, bars) for monitoring.",
177
+ handler: async ({ sessionId, limit = 50 } = {}) => {
178
+ const session = requireSession(sessionId);
179
+ return session.recentEvents(limit);
180
+ },
181
+ },
182
+
183
+ attach_strategy: {
184
+ description:
185
+ "Attach a named built-in strategy to a session. It will auto-evaluate on each feed_price and place orders when flat.",
186
+ handler: async ({ sessionId, strategy, params = {} } = {}) => {
187
+ const session = requireSession(sessionId);
188
+ const factory = getStrategy(strategy);
189
+ const signal = factory(params);
190
+ session._strategy = signal;
191
+ return { ok: true, strategy, params };
192
+ },
193
+ },
194
+
195
+ halt_all: {
196
+ description: "Emergency kill switch: flatten all positions and stop all trading sessions.",
197
+ handler: async () => {
198
+ await manager.haltAll();
199
+ return { ok: true, sessionsHalted: manager.list().length };
200
+ },
201
+ },
202
+ };
@@ -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: "1.1.0" });
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 mcpTools = {
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 };
@@ -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 < iterations; it += 1) {
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),