tradelab 1.0.1 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +188 -328
  3. package/bin/tradelab-mcp.js +7 -0
  4. package/bin/tradelab.js +29 -0
  5. package/dist/cjs/data.cjs +149 -26
  6. package/dist/cjs/index.cjs +1917 -1005
  7. package/dist/cjs/live.cjs +536 -25
  8. package/dist/cjs/ta.cjs +339 -0
  9. package/docs/README.md +32 -66
  10. package/docs/api-reference.md +283 -112
  11. package/docs/backtest-engine.md +210 -252
  12. package/docs/data-reporting-cli.md +114 -156
  13. package/docs/examples.md +6 -6
  14. package/docs/live-trading.md +263 -92
  15. package/docs/mcp.md +285 -0
  16. package/docs/research.md +157 -0
  17. package/examples/liveDashboard.js +33 -0
  18. package/examples/llmSignal.js +33 -0
  19. package/examples/mcpLiveTrading.js +77 -0
  20. package/examples/optimize.js +25 -0
  21. package/package.json +26 -4
  22. package/src/engine/asyncSignal.js +28 -0
  23. package/src/engine/backtest.js +13 -1
  24. package/src/engine/backtestAsync.js +27 -0
  25. package/src/engine/backtestTicks.js +13 -2
  26. package/src/engine/barSystemRunner.js +96 -41
  27. package/src/engine/execution.js +39 -0
  28. package/src/engine/grid.js +15 -0
  29. package/src/engine/llmSignal.js +84 -0
  30. package/src/engine/optimize.js +110 -0
  31. package/src/engine/optimizeWorker.js +67 -0
  32. package/src/engine/portfolio.js +4 -1
  33. package/src/engine/walkForward.js +1 -0
  34. package/src/index.js +9 -0
  35. package/src/live/dashboard/server.js +179 -0
  36. package/src/live/engine/liveEngine.js +2 -2
  37. package/src/live/engine/paperEngine.js +5 -0
  38. package/src/live/index.js +3 -0
  39. package/src/live/session.js +402 -0
  40. package/src/mcp/liveTools.js +179 -0
  41. package/src/mcp/schemas.js +167 -0
  42. package/src/mcp/server.js +35 -0
  43. package/src/mcp/tools.js +265 -0
  44. package/src/metrics/annualize.js +32 -0
  45. package/src/metrics/benchmark.js +55 -0
  46. package/src/metrics/buildMetrics.js +34 -13
  47. package/src/metrics/finite.js +17 -0
  48. package/src/research/combinations.js +18 -0
  49. package/src/research/cpcv.js +47 -0
  50. package/src/research/deflatedSharpe.js +35 -0
  51. package/src/research/index.js +6 -0
  52. package/src/research/monteCarlo.js +88 -0
  53. package/src/research/pbo.js +69 -0
  54. package/src/research/stats.js +78 -0
  55. package/src/strategies/builtins.js +96 -0
  56. package/src/strategies/index.js +30 -0
  57. package/src/ta/channels.js +67 -0
  58. package/src/ta/index.js +16 -0
  59. package/src/ta/oscillators.js +70 -0
  60. package/src/ta/trend.js +78 -0
  61. package/src/utils/random.js +33 -0
  62. package/templates/dashboard.html +661 -0
  63. package/types/index.d.ts +179 -0
  64. package/types/live.d.ts +114 -0
  65. package/types/mcp.d.ts +17 -0
  66. package/types/ta.d.ts +45 -0
package/bin/tradelab.js CHANGED
@@ -116,6 +116,22 @@ function createBrokerAdapter(args, overrides = {}) {
116
116
  throw new Error(`Unsupported broker "${brokerName}"`);
117
117
  }
118
118
 
119
+ async function maybeStartDashboard(args, source) {
120
+ if (!toBoolean(args.dashboard, false)) return null;
121
+ const { createDashboardServer } = await import("../src/live/dashboard/server.js");
122
+ const dashboard = createDashboardServer({
123
+ source,
124
+ port: toNumber(args.dashboardPort, 4317),
125
+ });
126
+ const url = await dashboard.start();
127
+ console.log(`dashboard: ${url}`);
128
+ return dashboard;
129
+ }
130
+
131
+ async function closeDashboard(dashboard) {
132
+ if (dashboard) await dashboard.close();
133
+ }
134
+
119
135
  function brokerConfigFromArgs(args, overrides = {}) {
120
136
  return {
121
137
  apiKey: overrides.apiKey ?? args.apiKey,
@@ -514,6 +530,7 @@ async function commandLive(args, overrides = {}) {
514
530
  maxDailyLossPct: toNumber(fileConfig.maxDailyLossPct ?? args.maxDailyLossPct, 0),
515
531
  equity: toNumber(fileConfig.equity ?? args.equity, 10_000),
516
532
  });
533
+ const dashboard = await maybeStartDashboard(args, orchestrator);
517
534
  await broker.connect(brokerConfig);
518
535
  await orchestrator.start();
519
536
 
@@ -526,6 +543,15 @@ async function commandLive(args, overrides = {}) {
526
543
 
527
544
  if (!watch) {
528
545
  await orchestrator.stop();
546
+ await closeDashboard(dashboard);
547
+ } else if (dashboard) {
548
+ const shutdown = async () => {
549
+ await orchestrator.stop();
550
+ await closeDashboard(dashboard);
551
+ process.exit(0);
552
+ };
553
+ process.once("SIGINT", shutdown);
554
+ process.once("SIGTERM", shutdown);
529
555
  }
530
556
  return;
531
557
  }
@@ -551,6 +577,7 @@ async function commandLive(args, overrides = {}) {
551
577
  storage,
552
578
  brokerConfig,
553
579
  });
580
+ const dashboard = await maybeStartDashboard(args, engine);
554
581
 
555
582
  await engine.start();
556
583
 
@@ -563,11 +590,13 @@ async function commandLive(args, overrides = {}) {
563
590
 
564
591
  if (!watch) {
565
592
  await engine.stop();
593
+ await closeDashboard(dashboard);
566
594
  return;
567
595
  }
568
596
 
569
597
  const shutdown = async () => {
570
598
  await engine.stop();
599
+ await closeDashboard(dashboard);
571
600
  process.exit(0);
572
601
  };
573
602
  process.once("SIGINT", shutdown);
package/dist/cjs/data.cjs CHANGED
@@ -119,22 +119,93 @@ function calculatePositionSize({
119
119
  return quantity >= minQty ? quantity : 0;
120
120
  }
121
121
 
122
+ // src/metrics/finite.js
123
+ var BIG_NUMBER = 1e9;
124
+ function clampFinite(value, fallback = 0) {
125
+ if (value === Infinity) return BIG_NUMBER;
126
+ if (value === -Infinity) return -BIG_NUMBER;
127
+ if (typeof value === "number" && Number.isFinite(value)) return value;
128
+ return fallback;
129
+ }
130
+
131
+ // src/metrics/annualize.js
132
+ var TRADING_DAYS = 252;
133
+ var RTH_HOURS = 6.5;
134
+ var MS_PER_YEAR = 365 * 24 * 60 * 60 * 1e3;
135
+ var INTERVAL_PERIODS = {
136
+ "1m": TRADING_DAYS * RTH_HOURS * 60,
137
+ "2m": TRADING_DAYS * RTH_HOURS * 30,
138
+ "5m": TRADING_DAYS * RTH_HOURS * 12,
139
+ "15m": TRADING_DAYS * RTH_HOURS * 4,
140
+ "30m": TRADING_DAYS * RTH_HOURS * 2,
141
+ "1h": TRADING_DAYS * RTH_HOURS,
142
+ "60m": TRADING_DAYS * RTH_HOURS,
143
+ "1d": TRADING_DAYS,
144
+ "1wk": 52,
145
+ "1mo": 12
146
+ };
147
+ function periodsPerYear(interval, estBarMs) {
148
+ if (interval && INTERVAL_PERIODS[interval]) return INTERVAL_PERIODS[interval];
149
+ if (Number.isFinite(estBarMs) && estBarMs > 0) {
150
+ return Math.round(MS_PER_YEAR / estBarMs);
151
+ }
152
+ return TRADING_DAYS;
153
+ }
154
+
155
+ // src/metrics/benchmark.js
156
+ function mean(xs) {
157
+ return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
158
+ }
159
+ function benchmarkStats(strategyReturns, benchmarkReturns) {
160
+ const nullStats = {
161
+ alpha: null,
162
+ beta: null,
163
+ correlation: null,
164
+ informationRatio: null,
165
+ trackingError: null
166
+ };
167
+ if (!Array.isArray(strategyReturns) || !Array.isArray(benchmarkReturns) || strategyReturns.length === 0 || strategyReturns.length !== benchmarkReturns.length) {
168
+ return nullStats;
169
+ }
170
+ const meanStrat = mean(strategyReturns);
171
+ const meanBench = mean(benchmarkReturns);
172
+ let covar = 0;
173
+ let varBench = 0;
174
+ let varStrat = 0;
175
+ for (let i = 0; i < strategyReturns.length; i += 1) {
176
+ const ds = strategyReturns[i] - meanStrat;
177
+ const db = benchmarkReturns[i] - meanBench;
178
+ covar += ds * db;
179
+ varBench += db * db;
180
+ varStrat += ds * ds;
181
+ }
182
+ const beta = varBench === 0 ? 0 : covar / varBench;
183
+ const alpha = meanStrat - beta * meanBench;
184
+ const denom = Math.sqrt(varStrat * varBench);
185
+ const correlation = denom === 0 ? 0 : covar / denom;
186
+ const active = strategyReturns.map((r, i) => r - benchmarkReturns[i]);
187
+ const meanActive = mean(active);
188
+ const trackingError = Math.sqrt(mean(active.map((a) => (a - meanActive) ** 2)));
189
+ const informationRatio = trackingError === 0 ? 0 : meanActive / trackingError;
190
+ return { alpha, beta, correlation, informationRatio, trackingError };
191
+ }
192
+
122
193
  // src/metrics/buildMetrics.js
123
194
  function sum(values) {
124
195
  return values.reduce((total, value) => total + value, 0);
125
196
  }
126
- function mean(values) {
197
+ function mean2(values) {
127
198
  return values.length ? sum(values) / values.length : 0;
128
199
  }
129
200
  function stddev(values) {
130
201
  if (values.length <= 1) return 0;
131
- const avg = mean(values);
132
- return Math.sqrt(mean(values.map((value) => (value - avg) ** 2)));
202
+ const avg = mean2(values);
203
+ return Math.sqrt(mean2(values.map((value) => (value - avg) ** 2)));
133
204
  }
134
205
  function sortino(values) {
135
206
  const losses = values.filter((value) => value < 0);
136
207
  const downsideDeviation = stddev(losses.length ? losses : [0]);
137
- const avg = mean(values);
208
+ const avg = mean2(values);
138
209
  return downsideDeviation === 0 ? avg > 0 ? Infinity : 0 : avg / downsideDeviation;
139
210
  }
140
211
  function dayKeyUTC(timeMs) {
@@ -220,14 +291,22 @@ function percentile(values, percentileRank) {
220
291
  const index = Math.floor((sorted.length - 1) * percentileRank);
221
292
  return sorted[index];
222
293
  }
223
- var PROFIT_FACTOR_CAP = 1e6;
224
294
  function finiteProfitFactor(grossProfit, grossLoss) {
225
295
  if (grossLoss === 0) {
226
- return grossProfit > 0 ? PROFIT_FACTOR_CAP : 0;
296
+ return grossProfit > 0 ? BIG_NUMBER : 0;
227
297
  }
228
298
  return grossProfit / grossLoss;
229
299
  }
230
- function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqSeries }) {
300
+ function buildMetrics({
301
+ closed,
302
+ equityStart,
303
+ equityFinal,
304
+ candles,
305
+ estBarMs,
306
+ eqSeries,
307
+ interval,
308
+ benchmarkReturns
309
+ }) {
231
310
  const legs = [...closed].sort((left, right) => left.exit.time - right.exit.time);
232
311
  const completedTrades = [];
233
312
  const tradeRs = [];
@@ -297,11 +376,11 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
297
376
  if (pnl > 0) shortTradeWins += 1;
298
377
  }
299
378
  }
300
- const avgR = mean(tradeRs);
379
+ const avgR = mean2(tradeRs);
301
380
  const { maxWin, maxLoss } = streaks(labels);
302
- const expectancy = mean(tradePnls);
381
+ const expectancy = mean2(tradePnls);
303
382
  const tradeReturnStd = stddev(tradeReturns);
304
- const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 : mean(tradeReturns) / tradeReturnStd;
383
+ const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 : mean2(tradeReturns) / tradeReturnStd;
305
384
  const sortinoPerTrade = sortino(tradeReturns);
306
385
  const profitFactorPositions = finiteProfitFactor(grossProfitPositions, grossLossPositions);
307
386
  const profitFactorLegs = finiteProfitFactor(grossProfitLegs, grossLossLegs);
@@ -309,11 +388,11 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
309
388
  const calmar = maxDrawdown === 0 ? returnPct > 0 ? Infinity : 0 : returnPct / maxDrawdown;
310
389
  const totalBars = Math.max(1, candles.length);
311
390
  const exposurePct = openBars / totalBars;
312
- const avgHoldMin = mean(holdDurationsMinutes);
391
+ const avgHoldMin = mean2(holdDurationsMinutes);
313
392
  const equitySeries = eqSeries && eqSeries.length ? eqSeries : buildEquitySeriesFromLegs({ legs, equityStart });
314
393
  const dailyReturnsSeries = dailyReturns(equitySeries);
315
394
  const dailyStd = stddev(dailyReturnsSeries);
316
- const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 : mean(dailyReturnsSeries) / dailyStd;
395
+ const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 : mean2(dailyReturnsSeries) / dailyStd;
317
396
  const sortinoDaily = sortino(dailyReturnsSeries);
318
397
  const dailyWinRate = dailyReturnsSeries.length ? dailyReturnsSeries.filter((value) => value > 0).length / dailyReturnsSeries.length : 0;
319
398
  const rDistribution = {
@@ -335,28 +414,36 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
335
414
  trades: longTradesCount,
336
415
  winRate: longTradesCount ? longTradeWins / longTradesCount : 0,
337
416
  avgPnL: longTradesCount ? longPnLSum / longTradesCount : 0,
338
- avgR: mean(longRs)
417
+ avgR: mean2(longRs)
339
418
  },
340
419
  short: {
341
420
  trades: shortTradesCount,
342
421
  winRate: shortTradesCount ? shortTradeWins / shortTradesCount : 0,
343
422
  avgPnL: shortTradesCount ? shortPnLSum / shortTradesCount : 0,
344
- avgR: mean(shortRs)
423
+ avgR: mean2(shortRs)
345
424
  }
346
425
  };
426
+ const periods = periodsPerYear(interval, estBarMs);
427
+ const sqrtPeriods = Math.sqrt(periods);
428
+ const sharpeAnnualized = clampFinite(clampFinite(sharpeDaily) * sqrtPeriods);
429
+ const sortinoAnnualized = clampFinite(clampFinite(sortinoDaily) * sqrtPeriods);
430
+ const benchmark = benchmarkStats(dailyReturnsSeries, benchmarkReturns ?? []);
347
431
  return {
348
432
  trades: completedTrades.length,
349
433
  winRate: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
350
- profitFactor: profitFactorPositions,
434
+ profitFactor: clampFinite(profitFactorPositions),
351
435
  expectancy,
352
436
  totalR,
353
437
  avgR,
354
- sharpe: sharpeDaily,
355
- sharpePerTrade,
356
- sortinoPerTrade,
438
+ sharpe: clampFinite(sharpeDaily),
439
+ sharpeAnnualized,
440
+ sortinoAnnualized,
441
+ sharpePerTrade: clampFinite(sharpePerTrade),
442
+ sortinoPerTrade: clampFinite(sortinoPerTrade),
443
+ annualizationPeriods: periods,
357
444
  maxDrawdown,
358
445
  maxDrawdownPct: maxDrawdown,
359
- calmar,
446
+ calmar: clampFinite(calmar),
360
447
  maxConsecWins: maxWin,
361
448
  maxConsecLosses: maxLoss,
362
449
  avgHold: avgHoldMin,
@@ -366,12 +453,13 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
366
453
  returnPct,
367
454
  finalEquity: equityFinal,
368
455
  startEquity: equityStart,
369
- profitFactor_pos: profitFactorPositions,
370
- profitFactor_leg: profitFactorLegs,
456
+ profitFactor_pos: clampFinite(profitFactorPositions),
457
+ profitFactor_leg: clampFinite(profitFactorLegs),
371
458
  winRate_pos: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
372
459
  winRate_leg: legs.length ? winningLegCount / legs.length : 0,
373
- sharpeDaily,
374
- sortinoDaily,
460
+ sharpeDaily: clampFinite(sharpeDaily),
461
+ sortinoDaily: clampFinite(sortinoDaily),
462
+ benchmark,
375
463
  sideBreakdown,
376
464
  long: sideBreakdown.long,
377
465
  short: sideBreakdown.short,
@@ -380,7 +468,7 @@ function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqS
380
468
  daily: {
381
469
  count: dailyReturnsSeries.length,
382
470
  winRate: dailyWinRate,
383
- avgReturn: mean(dailyReturnsSeries)
471
+ avgReturn: mean2(dailyReturnsSeries)
384
472
  }
385
473
  };
386
474
  }
@@ -800,6 +888,30 @@ function dayKeyET(timeMs) {
800
888
  const pseudoEtTime = anchor.getTime() + hoursET * 60 * 60 * 1e3 + minutesETDay * 60 * 1e3;
801
889
  return dayKeyUTC2(pseudoEtTime);
802
890
  }
891
+ var MS_PER_YEAR2 = 365 * 24 * 60 * 60 * 1e3;
892
+ function fundingEvents(fromMs, toMs, intervalMs, anchorMs = 0) {
893
+ if (!(intervalMs > 0) || toMs <= fromMs) return 0;
894
+ const firstK = Math.floor((fromMs - anchorMs) / intervalMs) + 1;
895
+ const lastK = Math.floor((toMs - anchorMs) / intervalMs);
896
+ return Math.max(0, lastK - firstK + 1);
897
+ }
898
+ function financingCost({ side, notional, fromMs, toMs, costs }) {
899
+ const model = costs || {};
900
+ const absNotional = Math.abs(notional);
901
+ let cost = 0;
902
+ if (model.carry) {
903
+ const annualBps = side === "long" ? model.carry.longAnnualBps ?? 0 : model.carry.shortAnnualBps ?? 0;
904
+ const years = Math.max(0, toMs - fromMs) / MS_PER_YEAR2;
905
+ cost += absNotional * (annualBps / 1e4) * years;
906
+ }
907
+ const funding = model.funding;
908
+ if (funding && funding.intervalMs > 0 && Number.isFinite(funding.rateBps)) {
909
+ const count = fundingEvents(fromMs, toMs, funding.intervalMs, funding.anchorMs ?? 0);
910
+ const perEvent = absNotional * (funding.rateBps / 1e4);
911
+ cost += (side === "long" ? 1 : -1) * perEvent * count;
912
+ }
913
+ return cost;
914
+ }
803
915
 
804
916
  // src/engine/backtest.js
805
917
  function asNumber(value) {
@@ -931,6 +1043,7 @@ function mergeOptions(options) {
931
1043
  maxSlipROnFill: options.maxSlipROnFill ?? 0.4,
932
1044
  collectEqSeries: options.collectEqSeries ?? true,
933
1045
  collectReplay: options.collectReplay ?? true,
1046
+ benchmarkReturns: Array.isArray(options.benchmarkReturns) ? options.benchmarkReturns : null,
934
1047
  strict: options.strict ?? false
935
1048
  };
936
1049
  }
@@ -1053,7 +1166,14 @@ function backtest(rawOptions) {
1053
1166
  const entryFill = openPos.entryFill;
1054
1167
  const grossPnl = (exitPx - entryFill) * direction * qty;
1055
1168
  const entryFeePortion = (openPos.entryFeeTotal || 0) * (qty / openPos.initSize);
1056
- const pnl = grossPnl - entryFeePortion - exitFeeTotal;
1169
+ const financing = financingCost({
1170
+ side: openPos.side,
1171
+ notional: entryFill * qty,
1172
+ fromMs: openPos.openTime,
1173
+ toMs: time,
1174
+ costs
1175
+ });
1176
+ const pnl = grossPnl - entryFeePortion - exitFeeTotal - financing;
1057
1177
  currentEquity += pnl;
1058
1178
  dayPnl += pnl;
1059
1179
  if (wantEqSeries) {
@@ -1081,6 +1201,7 @@ function backtest(rawOptions) {
1081
1201
  time,
1082
1202
  reason,
1083
1203
  pnl,
1204
+ financing,
1084
1205
  exitATR: openPos._lastATR ?? void 0
1085
1206
  },
1086
1207
  mfeR: openPos._mfeR ?? 0,
@@ -1483,7 +1604,9 @@ function backtest(rawOptions) {
1483
1604
  equityFinal: currentEquity,
1484
1605
  candles,
1485
1606
  estBarMs: estimatedBarMs,
1486
- eqSeries
1607
+ eqSeries,
1608
+ interval: options.interval,
1609
+ benchmarkReturns: options.benchmarkReturns
1487
1610
  });
1488
1611
  const positions = closed.filter((trade) => trade.exit.reason !== "SCALE");
1489
1612
  const lastPrice = asNumber(candles[candles.length - 1]?.close);