tradeblocks-mcp 3.0.2 → 3.0.3

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 (214) hide show
  1. package/README.md +82 -68
  2. package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
  3. package/dist/chunk-27S67XW3.js.map +1 -0
  4. package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
  5. package/dist/chunk-2E63THNI.js.map +1 -0
  6. package/dist/{chunk-BKQ4PM4Y.js → chunk-6S37CXUA.js} +11 -13
  7. package/dist/chunk-6S37CXUA.js.map +1 -0
  8. package/dist/{chunk-PRAYH3RT.js → chunk-AP7IHUUR.js} +29 -12
  9. package/dist/chunk-AP7IHUUR.js.map +1 -0
  10. package/dist/{chunk-QTTR7AAW.js → chunk-C6LL746C.js} +67 -84
  11. package/dist/{chunk-QTTR7AAW.js.map → chunk-C6LL746C.js.map} +1 -1
  12. package/dist/{chunk-W2PP3LEH.js → chunk-SEUZYQGQ.js} +17 -8
  13. package/dist/chunk-SEUZYQGQ.js.map +1 -0
  14. package/dist/{chunk-4BLCXNQ6.js → chunk-UBUC5A66.js} +105 -114
  15. package/dist/chunk-UBUC5A66.js.map +1 -0
  16. package/dist/{chunk-XXYOUIZY.js → chunk-VDU25Z6X.js} +63 -80
  17. package/dist/chunk-VDU25Z6X.js.map +1 -0
  18. package/dist/daily-log-processor-BY3ISY6K.js +9 -0
  19. package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
  20. package/dist/iv-solver-worker.js +9 -1
  21. package/dist/market-provider-H3ARIVZ4.js +16 -0
  22. package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
  23. package/dist/{sync-PO4IPCYV.js → sync-EVLKB3ZJ.js} +7 -7
  24. package/dist/test-exports.js +358 -254
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-EYA3I3XB.js +9 -0
  27. package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
  28. package/manifest.json +3 -11
  29. package/package.json +1 -1
  30. package/server/{chunk-NZO6PT64.js → chunk-4P7D7YZP.js} +102 -113
  31. package/server/chunk-4P7D7YZP.js.map +1 -0
  32. package/server/{chunk-72GKJE2U.js → chunk-BOPHW5M6.js} +521 -235
  33. package/server/chunk-BOPHW5M6.js.map +1 -0
  34. package/server/{chunk-WA5AAPCH.js → chunk-GH2552SE.js} +17 -8
  35. package/server/chunk-GH2552SE.js.map +1 -0
  36. package/server/{chunk-5EBXHT6C.js → chunk-OBYKFW2B.js} +11 -13
  37. package/server/chunk-OBYKFW2B.js.map +1 -0
  38. package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
  39. package/server/chunk-T66KH2XH.js.map +1 -0
  40. package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
  41. package/server/chunk-W5E7FHC4.js.map +1 -0
  42. package/server/{chunk-CSDVJPBB.js → chunk-YUCOAJ4Z.js} +29 -12
  43. package/server/chunk-YUCOAJ4Z.js.map +1 -0
  44. package/server/{chunk-PNKG7RY7.js → chunk-ZBJCF4ZG.js} +63 -80
  45. package/server/chunk-ZBJCF4ZG.js.map +1 -0
  46. package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
  47. package/server/config-DK7KOMNL.js.map +1 -0
  48. package/server/daily-log-processor-ENEUT22A.js +10 -0
  49. package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
  50. package/server/http-server.js +2 -5
  51. package/server/index.js +1771 -1538
  52. package/server/index.js.map +1 -1
  53. package/server/iv-solver-worker.js +9 -1
  54. package/server/market-provider-B437HKLW.js +17 -0
  55. package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
  56. package/server/{sync-QFI5L7S7.js → sync-V25UQJA3.js} +7 -7
  57. package/server/trade-processor-L3PIQ5TG.js +10 -0
  58. package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
  59. package/src/auth/clients-store.ts +4 -4
  60. package/src/auth/code-store.ts +1 -1
  61. package/src/auth/config.ts +11 -12
  62. package/src/auth/login-page.ts +8 -10
  63. package/src/auth/provider.ts +35 -30
  64. package/src/auth/token.ts +17 -15
  65. package/src/db/backtest-schemas.ts +12 -4
  66. package/src/db/connection.ts +107 -40
  67. package/src/db/index.ts +37 -5
  68. package/src/db/json-adapters.ts +22 -31
  69. package/src/db/json-migration.ts +11 -15
  70. package/src/db/json-store.ts +5 -3
  71. package/src/db/market-datasets.ts +48 -15
  72. package/src/db/market-schemas.ts +11 -5
  73. package/src/db/market-views.ts +119 -32
  74. package/src/db/parquet-writer.ts +7 -7
  75. package/src/db/profile-schemas.ts +39 -22
  76. package/src/db/schemas.ts +2 -2
  77. package/src/http-server.ts +7 -12
  78. package/src/index.ts +8 -9
  79. package/src/market/ingestor/index.ts +1 -4
  80. package/src/market/ingestor/market-ingestor.ts +140 -67
  81. package/src/market/ingestor/types.ts +1 -3
  82. package/src/market/stores/chain-sql.ts +2 -8
  83. package/src/market/stores/chain-store.ts +1 -4
  84. package/src/market/stores/duckdb-chain-store.ts +3 -14
  85. package/src/market/stores/duckdb-enriched-store.ts +3 -13
  86. package/src/market/stores/duckdb-quote-store.ts +10 -27
  87. package/src/market/stores/duckdb-spot-store.ts +4 -20
  88. package/src/market/stores/enriched-sql.ts +3 -9
  89. package/src/market/stores/enriched-store.ts +2 -2
  90. package/src/market/stores/parquet-chain-store.ts +5 -22
  91. package/src/market/stores/parquet-enriched-store.ts +4 -16
  92. package/src/market/stores/parquet-oi-daily-store.ts +3 -14
  93. package/src/market/stores/parquet-quote-store.ts +9 -29
  94. package/src/market/stores/parquet-spot-store.ts +14 -33
  95. package/src/market/stores/quote-store.ts +2 -10
  96. package/src/market/stores/spot-sql.ts +3 -15
  97. package/src/market/stores/spot-store.ts +1 -1
  98. package/src/market/tickers/defaults.json +6 -6
  99. package/src/market/tickers/loader.ts +2 -7
  100. package/src/market/tickers/registry.ts +5 -15
  101. package/src/market/tickers/schemas.ts +2 -6
  102. package/src/models/strategy-profile.ts +39 -39
  103. package/src/sync/block-sync.ts +44 -65
  104. package/src/sync/index.ts +1 -4
  105. package/src/sync/metadata.ts +14 -20
  106. package/src/test-exports.ts +127 -123
  107. package/src/tools/analysis.ts +180 -159
  108. package/src/tools/batch-exit-analysis.ts +100 -59
  109. package/src/tools/blocks/analysis.ts +116 -101
  110. package/src/tools/blocks/comparison.ts +281 -364
  111. package/src/tools/blocks/core.ts +208 -253
  112. package/src/tools/blocks/health.ts +571 -563
  113. package/src/tools/blocks/similarity.ts +316 -283
  114. package/src/tools/edge-decay.ts +124 -197
  115. package/src/tools/exit-analysis.ts +130 -77
  116. package/src/tools/greeks-attribution.ts +84 -35
  117. package/src/tools/guides.ts +4 -6
  118. package/src/tools/imports.ts +9 -12
  119. package/src/tools/market-data.ts +1067 -757
  120. package/src/tools/market-enrichment.ts +3 -3
  121. package/src/tools/market-fetch.ts +148 -67
  122. package/src/tools/market-imports.ts +12 -12
  123. package/src/tools/middleware/sync-middleware.ts +5 -6
  124. package/src/tools/performance.ts +185 -302
  125. package/src/tools/profile-analysis.ts +52 -66
  126. package/src/tools/profiles.ts +106 -69
  127. package/src/tools/regime-advisor.ts +20 -45
  128. package/src/tools/replay.ts +81 -77
  129. package/src/tools/reports/discrepancies.ts +298 -328
  130. package/src/tools/reports/fields.ts +7 -25
  131. package/src/tools/reports/helpers.ts +18 -49
  132. package/src/tools/reports/predictive.ts +27 -70
  133. package/src/tools/reports/slippage-trends.ts +315 -345
  134. package/src/tools/reports/slippage.ts +1 -4
  135. package/src/tools/reports/strategy-matches.ts +399 -441
  136. package/src/tools/schema.ts +43 -40
  137. package/src/tools/shared/filters.ts +3 -9
  138. package/src/tools/snapshot.ts +9 -30
  139. package/src/tools/sql.ts +15 -14
  140. package/src/tools/tickers.ts +1 -4
  141. package/src/utils/batch-exit-analysis.ts +31 -42
  142. package/src/utils/black-scholes.ts +39 -29
  143. package/src/utils/block-loader.ts +68 -82
  144. package/src/utils/calibration-probe.ts +1 -2
  145. package/src/utils/chain-loader.ts +3 -3
  146. package/src/utils/csv-discovery.ts +16 -22
  147. package/src/utils/data-quality.ts +24 -36
  148. package/src/utils/exit-triggers.ts +91 -96
  149. package/src/utils/field-timing.ts +94 -79
  150. package/src/utils/filter-predicates.ts +13 -9
  151. package/src/utils/flatfile-importer.ts +94 -64
  152. package/src/utils/greeks-decomposition.ts +152 -100
  153. package/src/utils/iv-solver-pool.ts +55 -25
  154. package/src/utils/iv-solver-worker.ts +5 -5
  155. package/src/utils/market-enricher.ts +528 -497
  156. package/src/utils/market-importer.ts +31 -12
  157. package/src/utils/market-provider.ts +21 -23
  158. package/src/utils/massive-tier.ts +5 -7
  159. package/src/utils/migrate-option-data-helpers.ts +2 -8
  160. package/src/utils/option-quote-greeks.ts +25 -31
  161. package/src/utils/option-time.ts +4 -8
  162. package/src/utils/output-formatter.ts +1 -4
  163. package/src/utils/provider-capabilities.ts +1 -4
  164. package/src/utils/providers/massive.ts +59 -93
  165. package/src/utils/providers/thetadata/backfill.ts +14 -23
  166. package/src/utils/providers/thetadata/client.ts +12 -8
  167. package/src/utils/providers/thetadata/decode.ts +2 -20
  168. package/src/utils/providers/thetadata/endpoints.ts +32 -40
  169. package/src/utils/providers/thetadata/join.ts +11 -10
  170. package/src/utils/providers/thetadata/proto.ts +12 -10
  171. package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
  172. package/src/utils/providers/thetadata.ts +11 -10
  173. package/src/utils/quote-enricher.ts +4 -4
  174. package/src/utils/quote-parquet-projection.ts +3 -11
  175. package/src/utils/sample-date-selector.ts +3 -5
  176. package/src/utils/schema-metadata.ts +102 -70
  177. package/src/utils/ticker.ts +5 -9
  178. package/src/utils/trade-replay.ts +77 -68
  179. package/dist/chunk-4BLCXNQ6.js.map +0 -1
  180. package/dist/chunk-BKQ4PM4Y.js.map +0 -1
  181. package/dist/chunk-FGZH632F.js.map +0 -1
  182. package/dist/chunk-LDKTV7GW.js.map +0 -1
  183. package/dist/chunk-PRAYH3RT.js.map +0 -1
  184. package/dist/chunk-W2PP3LEH.js.map +0 -1
  185. package/dist/chunk-XXYOUIZY.js.map +0 -1
  186. package/dist/daily-log-processor-6MWJ23JK.js +0 -9
  187. package/dist/market-provider-VDRJUEF2.js +0 -16
  188. package/dist/trade-processor-NHU4VWRX.js +0 -9
  189. package/server/chunk-5EBXHT6C.js.map +0 -1
  190. package/server/chunk-72GKJE2U.js.map +0 -1
  191. package/server/chunk-CSDVJPBB.js.map +0 -1
  192. package/server/chunk-FBNDMCT5.js.map +0 -1
  193. package/server/chunk-NRFXAJF7.js.map +0 -1
  194. package/server/chunk-NZO6PT64.js.map +0 -1
  195. package/server/chunk-PNKG7RY7.js.map +0 -1
  196. package/server/chunk-WA5AAPCH.js.map +0 -1
  197. package/server/config-6IZXEFEX.js.map +0 -1
  198. package/server/daily-log-processor-Y3PVSVBM.js +0 -10
  199. package/server/market-provider-VOYYVYWT.js +0 -17
  200. package/server/trade-processor-JWVS37KM.js +0 -10
  201. /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-BY3ISY6K.js.map} +0 -0
  202. /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
  203. /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-H3ARIVZ4.js.map} +0 -0
  204. /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
  205. /package/dist/{sync-PO4IPCYV.js.map → sync-EVLKB3ZJ.js.map} +0 -0
  206. /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-EYA3I3XB.js.map} +0 -0
  207. /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
  208. /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-ENEUT22A.js.map} +0 -0
  209. /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
  210. /package/server/{market-provider-VOYYVYWT.js.map → market-provider-B437HKLW.js.map} +0 -0
  211. /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
  212. /package/server/{sync-QFI5L7S7.js.map → sync-V25UQJA3.js.map} +0 -0
  213. /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-L3PIQ5TG.js.map} +0 -0
  214. /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
@@ -7,11 +7,7 @@
7
7
  import { z } from "zod";
8
8
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { loadBlock, loadReportingLog } from "../utils/block-loader.ts";
10
- import {
11
- createToolOutput,
12
- formatPercent,
13
- formatCurrency,
14
- } from "../utils/output-formatter.ts";
10
+ import { createToolOutput, formatPercent, formatCurrency } from "../utils/output-formatter.ts";
15
11
  import type { Trade, ReportingTrade } from "@tradeblocks/lib";
16
12
  import {
17
13
  normalizeToOneLot,
@@ -85,10 +81,7 @@ function computeTotalPremium(trade: Trade): number {
85
81
  /**
86
82
  * Calculate MFE/MAE metrics for a single trade
87
83
  */
88
- function calculateTradeExcursionMetrics(
89
- trade: Trade,
90
- tradeNumber: number
91
- ): MFEMAEDataPoint | null {
84
+ function calculateTradeExcursionMetrics(trade: Trade, tradeNumber: number): MFEMAEDataPoint | null {
92
85
  const totalMFE = computeTotalMaxProfit(trade);
93
86
  const totalMAE = computeTotalMaxLoss(trade);
94
87
 
@@ -100,9 +93,7 @@ function calculateTradeExcursionMetrics(
100
93
  // Determine denominator for percentage calculations
101
94
  const totalPremium = computeTotalPremium(trade);
102
95
  const margin =
103
- typeof trade.marginReq === "number" &&
104
- isFinite(trade.marginReq) &&
105
- trade.marginReq !== 0
96
+ typeof trade.marginReq === "number" && isFinite(trade.marginReq) && trade.marginReq !== 0
106
97
  ? Math.abs(trade.marginReq)
107
98
  : undefined;
108
99
 
@@ -175,14 +166,10 @@ function calculateMFEMAEData(trades: Trade[]): MFEMAEDataPoint[] {
175
166
  */
176
167
  function createExcursionDistribution(
177
168
  dataPoints: MFEMAEDataPoint[],
178
- bucketSize: number = 10
169
+ bucketSize: number = 10,
179
170
  ): MFEMAEDistributionBucket[] {
180
- const mfeValues = dataPoints
181
- .filter((d) => d.mfePercent !== undefined)
182
- .map((d) => d.mfePercent!);
183
- const maeValues = dataPoints
184
- .filter((d) => d.maePercent !== undefined)
185
- .map((d) => d.maePercent!);
171
+ const mfeValues = dataPoints.filter((d) => d.mfePercent !== undefined).map((d) => d.mfePercent!);
172
+ const maeValues = dataPoints.filter((d) => d.maePercent !== undefined).map((d) => d.maePercent!);
186
173
 
187
174
  if (mfeValues.length === 0 && maeValues.length === 0) {
188
175
  return [];
@@ -221,18 +208,14 @@ function createExcursionDistribution(
221
208
  */
222
209
  function filterByStrategy(trades: Trade[], strategy?: string): Trade[] {
223
210
  if (!strategy) return trades;
224
- return trades.filter(
225
- (t) => t.strategy.toLowerCase() === strategy.toLowerCase()
226
- );
211
+ return trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
227
212
  }
228
213
 
229
214
  /**
230
215
  * Get the ISO week number
231
216
  */
232
217
  function getISOWeekNumber(date: Date): number {
233
- const d = new Date(
234
- Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
235
- );
218
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
236
219
  const dayNum = d.getUTCDay() || 7;
237
220
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
238
221
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
@@ -253,8 +236,7 @@ function buildEquityCurve(trades: Trade[]): Array<{
253
236
  }
254
237
 
255
238
  const sortedTrades = [...trades].sort(
256
- (a, b) =>
257
- new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
239
+ (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
258
240
  );
259
241
 
260
242
  // Calculate initial capital from first trade
@@ -300,7 +282,7 @@ function buildEquityCurve(trades: Trade[]): Array<{
300
282
  * Calculate drawdown series from equity curve
301
283
  */
302
284
  function buildDrawdownSeries(
303
- equityCurve: Array<{ date: string; equity: number; highWaterMark: number }>
285
+ equityCurve: Array<{ date: string; equity: number; highWaterMark: number }>,
304
286
  ): Array<{ date: string; drawdownPct: number }> {
305
287
  return equityCurve.map((point) => ({
306
288
  date: point.date,
@@ -314,9 +296,7 @@ function buildDrawdownSeries(
314
296
  /**
315
297
  * Calculate monthly returns matrix
316
298
  */
317
- function buildMonthlyReturns(
318
- trades: Trade[]
319
- ): Record<number, Record<number, number>> {
299
+ function buildMonthlyReturns(trades: Trade[]): Record<number, Record<number, number>> {
320
300
  const monthlyData: Record<string, number> = {};
321
301
 
322
302
  trades.forEach((trade) => {
@@ -352,7 +332,7 @@ function buildMonthlyReturns(
352
332
  */
353
333
  function buildReturnDistribution(
354
334
  trades: Trade[],
355
- bucketCount: number = 20
335
+ bucketCount: number = 20,
356
336
  ): Array<{ rangeStart: number; rangeEnd: number; count: number }> {
357
337
  if (trades.length === 0) return [];
358
338
 
@@ -362,8 +342,7 @@ function buildReturnDistribution(
362
342
  const range = maxReturn - minReturn || 1;
363
343
  const bucketSize = range / bucketCount;
364
344
 
365
- const buckets: Array<{ rangeStart: number; rangeEnd: number; count: number }> =
366
- [];
345
+ const buckets: Array<{ rangeStart: number; rangeEnd: number; count: number }> = [];
367
346
 
368
347
  for (let i = 0; i < bucketCount; i++) {
369
348
  const rangeStart = minReturn + i * bucketSize;
@@ -383,24 +362,14 @@ function buildReturnDistribution(
383
362
  /**
384
363
  * Calculate day of week average P/L
385
364
  */
386
- function buildDayOfWeekData(
387
- trades: Trade[]
388
- ): Array<{
365
+ function buildDayOfWeekData(trades: Trade[]): Array<{
389
366
  day: string;
390
367
  count: number;
391
368
  avgPl: number;
392
369
  totalPl: number;
393
370
  avgPlPercent: number;
394
371
  }> {
395
- const dayNames = [
396
- "Monday",
397
- "Tuesday",
398
- "Wednesday",
399
- "Thursday",
400
- "Friday",
401
- "Saturday",
402
- "Sunday",
403
- ];
372
+ const dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
404
373
  const dayData: Record<
405
374
  string,
406
375
  { count: number; totalPl: number; totalPlPercent: number; percentCount: number }
@@ -428,13 +397,10 @@ function buildDayOfWeekData(
428
397
  return dayNames.map((day) => ({
429
398
  day,
430
399
  count: dayData[day]?.count || 0,
431
- avgPl:
432
- dayData[day]?.count > 0 ? dayData[day].totalPl / dayData[day].count : 0,
400
+ avgPl: dayData[day]?.count > 0 ? dayData[day].totalPl / dayData[day].count : 0,
433
401
  totalPl: dayData[day]?.totalPl || 0,
434
402
  avgPlPercent:
435
- dayData[day]?.percentCount > 0
436
- ? dayData[day].totalPlPercent / dayData[day].percentCount
437
- : 0,
403
+ dayData[day]?.percentCount > 0 ? dayData[day].totalPlPercent / dayData[day].percentCount : 0,
438
404
  }));
439
405
  }
440
406
 
@@ -460,8 +426,7 @@ function buildStreakData(trades: Trade[]): {
460
426
  } | null;
461
427
  } {
462
428
  const sortedTrades = [...trades].sort(
463
- (a, b) =>
464
- new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
429
+ (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
465
430
  );
466
431
 
467
432
  const winStreaks: number[] = [];
@@ -517,13 +482,9 @@ function buildStreakData(trades: Trade[]): {
517
482
  maxWinStreak: Math.max(...winStreaks, 0),
518
483
  maxLossStreak: Math.max(...lossStreaks, 0),
519
484
  avgWinStreak:
520
- winStreaks.length > 0
521
- ? winStreaks.reduce((a, b) => a + b) / winStreaks.length
522
- : 0,
485
+ winStreaks.length > 0 ? winStreaks.reduce((a, b) => a + b) / winStreaks.length : 0,
523
486
  avgLossStreak:
524
- lossStreaks.length > 0
525
- ? lossStreaks.reduce((a, b) => a + b) / lossStreaks.length
526
- : 0,
487
+ lossStreaks.length > 0 ? lossStreaks.reduce((a, b) => a + b) / lossStreaks.length : 0,
527
488
  },
528
489
  runsTest,
529
490
  };
@@ -558,8 +519,7 @@ function calculateRunsTest(trades: Trade[]): {
558
519
 
559
520
  const n = n1 + n0;
560
521
  const expectedRuns = (2 * n1 * n0) / n + 1;
561
- const variance =
562
- (2 * n1 * n0 * (2 * n1 * n0 - n)) / (n * n * (n - 1));
522
+ const variance = (2 * n1 * n0 * (2 * n1 * n0 - n)) / (n * n * (n - 1));
563
523
  const stdDev = Math.sqrt(variance);
564
524
 
565
525
  const zScore = stdDev > 0 ? (numRuns - expectedRuns) / stdDev : 0;
@@ -603,8 +563,7 @@ function normalCDF(x: number): number {
603
563
  x = Math.abs(x) / Math.sqrt(2);
604
564
 
605
565
  const t = 1.0 / (1.0 + p * x);
606
- const y =
607
- 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
566
+ const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
608
567
 
609
568
  return 0.5 * (1.0 + sign * y);
610
569
  }
@@ -612,9 +571,7 @@ function normalCDF(x: number): number {
612
571
  /**
613
572
  * Build trade sequence data (P&L by trade number with ROM)
614
573
  */
615
- function buildTradeSequence(
616
- trades: Trade[]
617
- ): Array<{
574
+ function buildTradeSequence(trades: Trade[]): Array<{
618
575
  tradeNumber: number;
619
576
  pl: number;
620
577
  rom: number | null;
@@ -624,9 +581,7 @@ function buildTradeSequence(
624
581
  }> {
625
582
  return trades.map((trade, index) => {
626
583
  const marginReq =
627
- typeof trade.marginReq === "number" && isFinite(trade.marginReq)
628
- ? trade.marginReq
629
- : null;
584
+ typeof trade.marginReq === "number" && isFinite(trade.marginReq) ? trade.marginReq : null;
630
585
  return {
631
586
  tradeNumber: index + 1,
632
587
  pl: trade.pl,
@@ -642,7 +597,7 @@ function buildTradeSequence(
642
597
  * Build ROM timeline (Return on Margin over time)
643
598
  */
644
599
  function buildRomTimeline(
645
- trades: Trade[]
600
+ trades: Trade[],
646
601
  ): Array<{ date: string; rom: number; tradeNumber: number }> {
647
602
  return trades
648
603
  .map((trade, index) => {
@@ -663,7 +618,7 @@ function buildRomTimeline(
663
618
  */
664
619
  function buildRollingMetrics(
665
620
  trades: Trade[],
666
- windowSize: number = 30
621
+ windowSize: number = 30,
667
622
  ): Array<{
668
623
  date: string;
669
624
  tradeNumber: number;
@@ -769,9 +724,7 @@ function buildRollingMetrics(
769
724
  /**
770
725
  * Build exit reason breakdown
771
726
  */
772
- function buildExitReasonBreakdown(
773
- trades: Trade[]
774
- ): Array<{
727
+ function buildExitReasonBreakdown(trades: Trade[]): Array<{
775
728
  reason: string;
776
729
  count: number;
777
730
  avgPl: number;
@@ -785,9 +738,7 @@ function buildExitReasonBreakdown(
785
738
 
786
739
  trades.forEach((trade) => {
787
740
  const reason =
788
- trade.reasonForClose && trade.reasonForClose.trim()
789
- ? trade.reasonForClose.trim()
790
- : "Unknown";
741
+ trade.reasonForClose && trade.reasonForClose.trim() ? trade.reasonForClose.trim() : "Unknown";
791
742
  const current = summaryMap.get(reason) || {
792
743
  count: 0,
793
744
  totalPl: 0,
@@ -819,9 +770,7 @@ function buildExitReasonBreakdown(
819
770
  /**
820
771
  * Build holding periods data
821
772
  */
822
- function buildHoldingPeriods(
823
- trades: Trade[]
824
- ): Array<{
773
+ function buildHoldingPeriods(trades: Trade[]): Array<{
825
774
  tradeNumber: number;
826
775
  dateOpened: string;
827
776
  dateClosed: string | null;
@@ -854,9 +803,7 @@ function buildHoldingPeriods(
854
803
  /**
855
804
  * Build premium efficiency data
856
805
  */
857
- function buildPremiumEfficiency(
858
- trades: Trade[]
859
- ): Array<{
806
+ function buildPremiumEfficiency(trades: Trade[]): Array<{
860
807
  tradeNumber: number;
861
808
  date: string;
862
809
  pl: number;
@@ -866,9 +813,7 @@ function buildPremiumEfficiency(
866
813
  }> {
867
814
  return trades.map((trade, index) => {
868
815
  const premium =
869
- typeof trade.premium === "number" && isFinite(trade.premium)
870
- ? trade.premium
871
- : null;
816
+ typeof trade.premium === "number" && isFinite(trade.premium) ? trade.premium : null;
872
817
  let efficiencyPct: number | null = null;
873
818
  if (premium !== null && premium !== 0) {
874
819
  efficiencyPct = (trade.pl / Math.abs(premium)) * 100;
@@ -897,7 +842,7 @@ function buildPremiumEfficiency(
897
842
  */
898
843
  function buildMarginUtilization(
899
844
  trades: Trade[],
900
- equityCurve?: Array<{ date: string; equity: number; tradeNumber: number }>
845
+ equityCurve?: Array<{ date: string; equity: number; tradeNumber: number }>,
901
846
  ): Array<{
902
847
  tradeNumber: number;
903
848
  date: string;
@@ -920,9 +865,7 @@ function buildMarginUtilization(
920
865
  return trades
921
866
  .map((trade, index) => {
922
867
  const marginReq =
923
- typeof trade.marginReq === "number" && isFinite(trade.marginReq)
924
- ? trade.marginReq
925
- : 0;
868
+ typeof trade.marginReq === "number" && isFinite(trade.marginReq) ? trade.marginReq : 0;
926
869
  const numContracts =
927
870
  typeof trade.numContracts === "number" && isFinite(trade.numContracts)
928
871
  ? trade.numContracts
@@ -934,7 +877,8 @@ function buildMarginUtilization(
934
877
  if (equityCurve && equityCurve.length > 0) {
935
878
  const tradeNumber = index + 1;
936
879
  const equityValue = equityByTradeNumber.get(tradeNumber);
937
- fundsAtClose = equityValue ??
880
+ fundsAtClose =
881
+ equityValue ??
938
882
  (typeof trade.fundsAtClose === "number" && isFinite(trade.fundsAtClose)
939
883
  ? trade.fundsAtClose
940
884
  : 0);
@@ -963,9 +907,7 @@ function buildMarginUtilization(
963
907
  /**
964
908
  * Build volatility regimes data (VIX-correlated)
965
909
  */
966
- function buildVolatilityRegimes(
967
- trades: Trade[]
968
- ): Array<{
910
+ function buildVolatilityRegimes(trades: Trade[]): Array<{
969
911
  tradeNumber: number;
970
912
  date: string;
971
913
  openingVix: number | null;
@@ -992,10 +934,7 @@ function buildVolatilityRegimes(
992
934
  openingVix,
993
935
  closingVix,
994
936
  pl: trade.pl,
995
- rom:
996
- trade.marginReq && trade.marginReq > 0
997
- ? (trade.pl / trade.marginReq) * 100
998
- : null,
937
+ rom: trade.marginReq && trade.marginReq > 0 ? (trade.pl / trade.marginReq) * 100 : null,
999
938
  };
1000
939
  })
1001
940
  .filter((item): item is NonNullable<typeof item> => item !== null);
@@ -1005,15 +944,12 @@ function buildVolatilityRegimes(
1005
944
  * Build monthly returns percent (percentage-based)
1006
945
  * Note: Uses trade-based calculation (initial capital derived from first trade)
1007
946
  */
1008
- function buildMonthlyReturnsPercent(
1009
- trades: Trade[]
1010
- ): Record<number, Record<number, number>> {
947
+ function buildMonthlyReturnsPercent(trades: Trade[]): Record<number, Record<number, number>> {
1011
948
  if (trades.length === 0) return {};
1012
949
 
1013
950
  // Sort trades by date
1014
951
  const sortedTrades = [...trades].sort(
1015
- (a, b) =>
1016
- new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
952
+ (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
1017
953
  );
1018
954
 
1019
955
  // Calculate initial capital from first trade
@@ -1098,11 +1034,7 @@ function buildMonthlyReturnsPercent(
1098
1034
  /**
1099
1035
  * Apply date range filter to trades
1100
1036
  */
1101
- function filterByDateRange(
1102
- trades: Trade[],
1103
- fromDate?: string,
1104
- toDate?: string
1105
- ): Trade[] {
1037
+ function filterByDateRange(trades: Trade[], fromDate?: string, toDate?: string): Trade[] {
1106
1038
  if (!fromDate && !toDate) return trades;
1107
1039
 
1108
1040
  return trades.filter((trade) => {
@@ -1143,7 +1075,7 @@ interface PeakExposure {
1143
1075
  */
1144
1076
  function buildDailyExposure(
1145
1077
  trades: Trade[],
1146
- equityCurve: Array<{ date: string; equity: number }>
1078
+ equityCurve: Array<{ date: string; equity: number }>,
1147
1079
  ): {
1148
1080
  dailyExposure: DailyExposurePoint[];
1149
1081
  peakDailyExposure: PeakExposure | null;
@@ -1176,10 +1108,7 @@ function buildDailyExposure(
1176
1108
  /**
1177
1109
  * Register all performance MCP tools
1178
1110
  */
1179
- export function registerPerformanceTools(
1180
- server: McpServer,
1181
- baseDir: string
1182
- ): void {
1111
+ export function registerPerformanceTools(server: McpServer, baseDir: string): void {
1183
1112
  // Tool 1: get_performance_charts
1184
1113
  server.registerTool(
1185
1114
  "get_performance_charts",
@@ -1188,10 +1117,7 @@ export function registerPerformanceTools(
1188
1117
  "Get chart data for performance visualizations: equity curves, drawdowns, return distributions, rolling metrics, and trade patterns. Use blockId from list_blocks.",
1189
1118
  inputSchema: z.object({
1190
1119
  blockId: z.string().describe("Block ID from list_blocks (e.g., 'main-port')"),
1191
- strategy: z
1192
- .string()
1193
- .optional()
1194
- .describe("Filter by strategy name (case-insensitive)"),
1120
+ strategy: z.string().optional().describe("Filter by strategy name (case-insensitive)"),
1195
1121
  charts: z
1196
1122
  .array(
1197
1123
  z.enum([
@@ -1212,22 +1138,16 @@ export function registerPerformanceTools(
1212
1138
  "volatility_regimes",
1213
1139
  "mfe_mae",
1214
1140
  "daily_exposure",
1215
- ])
1141
+ ]),
1216
1142
  )
1217
1143
  .default(["equity_curve", "drawdown", "monthly_returns"])
1218
1144
  .describe(
1219
- "Which charts to include. Options: equity_curve, drawdown, monthly_returns, monthly_returns_percent, return_distribution, day_of_week, streak_data (win/loss streaks + runs test), trade_sequence (P&L by trade #), rom_timeline (Return on Margin over time), rolling_metrics (30-trade rolling sharpe/win rate), exit_reason_breakdown, holding_periods, premium_efficiency, margin_utilization, volatility_regimes (VIX-correlated), mfe_mae (Maximum Favorable/Adverse Excursion for stop loss/take profit optimization), daily_exposure (daily margin exposure with peak tracking)"
1145
+ "Which charts to include. Options: equity_curve, drawdown, monthly_returns, monthly_returns_percent, return_distribution, day_of_week, streak_data (win/loss streaks + runs test), trade_sequence (P&L by trade #), rom_timeline (Return on Margin over time), rolling_metrics (30-trade rolling sharpe/win rate), exit_reason_breakdown, holding_periods, premium_efficiency, margin_utilization, volatility_regimes (VIX-correlated), mfe_mae (Maximum Favorable/Adverse Excursion for stop loss/take profit optimization), daily_exposure (daily margin exposure with peak tracking)",
1220
1146
  ),
1221
1147
  dateRange: z
1222
1148
  .object({
1223
- from: z
1224
- .string()
1225
- .optional()
1226
- .describe("Start date YYYY-MM-DD (inclusive)"),
1227
- to: z
1228
- .string()
1229
- .optional()
1230
- .describe("End date YYYY-MM-DD (inclusive)"),
1149
+ from: z.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
1150
+ to: z.string().optional().describe("End date YYYY-MM-DD (inclusive)"),
1231
1151
  })
1232
1152
  .optional()
1233
1153
  .describe("Filter trades to date range"),
@@ -1235,39 +1155,33 @@ export function registerPerformanceTools(
1235
1155
  .boolean()
1236
1156
  .default(false)
1237
1157
  .describe(
1238
- "Normalize all trades to 1 contract for fair comparison across different position sizes"
1158
+ "Normalize all trades to 1 contract for fair comparison across different position sizes",
1239
1159
  ),
1240
1160
  bucketCount: z
1241
1161
  .number()
1242
1162
  .min(5)
1243
1163
  .max(100)
1244
1164
  .default(20)
1245
- .describe(
1246
- "Number of histogram buckets for return_distribution (default: 20)"
1247
- ),
1165
+ .describe("Number of histogram buckets for return_distribution (default: 20)"),
1248
1166
  rollingWindowSize: z
1249
1167
  .number()
1250
1168
  .min(10)
1251
1169
  .max(100)
1252
1170
  .default(30)
1253
- .describe(
1254
- "Window size for rolling_metrics calculation (default: 30 trades)"
1255
- ),
1171
+ .describe("Window size for rolling_metrics calculation (default: 30 trades)"),
1256
1172
  mfeMaeBucketSize: z
1257
1173
  .number()
1258
1174
  .min(1)
1259
1175
  .max(50)
1260
1176
  .default(10)
1261
- .describe(
1262
- "Bucket size (in %) for MFE/MAE distribution histogram (default: 10%)"
1263
- ),
1177
+ .describe("Bucket size (in %) for MFE/MAE distribution histogram (default: 10%)"),
1264
1178
  maxDataPoints: z
1265
1179
  .number()
1266
1180
  .min(50)
1267
1181
  .max(10000)
1268
1182
  .default(500)
1269
1183
  .describe(
1270
- "Maximum data points for per-trade chart types (volatility_regimes, mfe_mae, trade_sequence, holding_periods, premium_efficiency, margin_utilization, rom_timeline). When exceeded, data is truncated with a flag. Default: 500."
1184
+ "Maximum data points for per-trade chart types (volatility_regimes, mfe_mae, trade_sequence, holding_periods, premium_efficiency, margin_utilization, rom_timeline). When exceeded, data is truncated with a flag. Default: 500.",
1271
1185
  ),
1272
1186
  }),
1273
1187
  },
@@ -1320,14 +1234,22 @@ export function registerPerformanceTools(
1320
1234
  let anyTruncated = false;
1321
1235
 
1322
1236
  // Helper to truncate per-trade arrays when they exceed maxDataPoints
1323
- function truncateArray<T>(arr: T[]): T[] | { data: T[]; truncated: true; totalPoints: number } {
1237
+ function truncateArray<T>(
1238
+ arr: T[],
1239
+ ): T[] | { data: T[]; truncated: true; totalPoints: number } {
1324
1240
  if (arr.length <= maxDataPoints) return arr;
1325
1241
  anyTruncated = true;
1326
- return { data: arr.slice(0, maxDataPoints), truncated: true as const, totalPoints: arr.length };
1242
+ return {
1243
+ data: arr.slice(0, maxDataPoints),
1244
+ truncated: true as const,
1245
+ totalPoints: arr.length,
1246
+ };
1327
1247
  }
1328
1248
 
1329
1249
  // Helper to count actual output length from possibly-truncated data
1330
- function outputLength<T>(result: T[] | { data: T[]; truncated: true; totalPoints: number }): number {
1250
+ function outputLength<T>(
1251
+ result: T[] | { data: T[]; truncated: true; totalPoints: number },
1252
+ ): number {
1331
1253
  return Array.isArray(result) ? result.length : result.data.length;
1332
1254
  }
1333
1255
 
@@ -1349,10 +1271,7 @@ export function registerPerformanceTools(
1349
1271
 
1350
1272
  if (charts.includes("monthly_returns")) {
1351
1273
  chartData.monthlyReturns = buildMonthlyReturns(trades);
1352
- const mr = chartData.monthlyReturns as Record<
1353
- number,
1354
- Record<number, number>
1355
- >;
1274
+ const mr = chartData.monthlyReturns as Record<number, Record<number, number>>;
1356
1275
  for (const year of Object.keys(mr)) {
1357
1276
  for (const month of Object.keys(mr[Number(year)])) {
1358
1277
  if (mr[Number(year)][Number(month)] !== 0) dataPoints++;
@@ -1362,10 +1281,7 @@ export function registerPerformanceTools(
1362
1281
 
1363
1282
  if (charts.includes("monthly_returns_percent")) {
1364
1283
  chartData.monthlyReturnsPercent = buildMonthlyReturnsPercent(trades);
1365
- const mrp = chartData.monthlyReturnsPercent as Record<
1366
- number,
1367
- Record<number, number>
1368
- >;
1284
+ const mrp = chartData.monthlyReturnsPercent as Record<number, Record<number, number>>;
1369
1285
  for (const year of Object.keys(mrp)) {
1370
1286
  for (const month of Object.keys(mrp[Number(year)])) {
1371
1287
  if (mrp[Number(year)][Number(month)] !== 0) dataPoints++;
@@ -1374,10 +1290,7 @@ export function registerPerformanceTools(
1374
1290
  }
1375
1291
 
1376
1292
  if (charts.includes("return_distribution")) {
1377
- chartData.returnDistribution = buildReturnDistribution(
1378
- trades,
1379
- bucketCount
1380
- );
1293
+ chartData.returnDistribution = buildReturnDistribution(trades, bucketCount);
1381
1294
  dataPoints += (chartData.returnDistribution as unknown[]).length;
1382
1295
  }
1383
1296
 
@@ -1410,10 +1323,7 @@ export function registerPerformanceTools(
1410
1323
  }
1411
1324
 
1412
1325
  if (charts.includes("rolling_metrics")) {
1413
- chartData.rollingMetrics = buildRollingMetrics(
1414
- trades,
1415
- rollingWindowSize
1416
- );
1326
+ chartData.rollingMetrics = buildRollingMetrics(trades, rollingWindowSize);
1417
1327
  dataPoints += (chartData.rollingMetrics as unknown[]).length;
1418
1328
  }
1419
1329
 
@@ -1466,13 +1376,11 @@ export function registerPerformanceTools(
1466
1376
 
1467
1377
  const avgMfePercent =
1468
1378
  dataWithMfe.length > 0
1469
- ? dataWithMfe.reduce((sum, d) => sum + (d.mfePercent || 0), 0) /
1470
- dataWithMfe.length
1379
+ ? dataWithMfe.reduce((sum, d) => sum + (d.mfePercent || 0), 0) / dataWithMfe.length
1471
1380
  : 0;
1472
1381
  const avgMaePercent =
1473
1382
  dataWithMae.length > 0
1474
- ? dataWithMae.reduce((sum, d) => sum + (d.maePercent || 0), 0) /
1475
- dataWithMae.length
1383
+ ? dataWithMae.reduce((sum, d) => sum + (d.maePercent || 0), 0) / dataWithMae.length
1476
1384
  : 0;
1477
1385
  const avgProfitCapture =
1478
1386
  mfeData.filter((d) => d.profitCapturePercent !== undefined).length > 0
@@ -1510,7 +1418,11 @@ export function registerPerformanceTools(
1510
1418
  let mfeOutputCount: number;
1511
1419
  if (simplifiedData.length > maxDataPoints) {
1512
1420
  anyTruncated = true;
1513
- mfeDataPointsOutput = { data: simplifiedData.slice(0, maxDataPoints), truncated: true, totalPoints: simplifiedData.length };
1421
+ mfeDataPointsOutput = {
1422
+ data: simplifiedData.slice(0, maxDataPoints),
1423
+ truncated: true,
1424
+ totalPoints: simplifiedData.length,
1425
+ };
1514
1426
  mfeOutputCount = maxDataPoints;
1515
1427
  } else {
1516
1428
  mfeDataPointsOutput = simplifiedData;
@@ -1572,7 +1484,8 @@ export function registerPerformanceTools(
1572
1484
  : 0,
1573
1485
  },
1574
1486
  ...(isStrategyFiltered && {
1575
- warning: "Percentage values may be misleading when filtering by strategy. " +
1487
+ warning:
1488
+ "Percentage values may be misleading when filtering by strategy. " +
1576
1489
  "Margin values are absolute (sized for the full portfolio), but the equity " +
1577
1490
  "curve is rebuilt for the filtered subset only. Use dollar exposure values " +
1578
1491
  "for accurate analysis when filtering.",
@@ -1585,9 +1498,7 @@ export function registerPerformanceTools(
1585
1498
  const filters: string[] = [];
1586
1499
  if (strategy) filters.push(`strategy=${strategy}`);
1587
1500
  if (dateRange?.from || dateRange?.to) {
1588
- filters.push(
1589
- `date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
1590
- );
1501
+ filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
1591
1502
  }
1592
1503
  if (normalizeTo1Lot) filters.push("normalized");
1593
1504
 
@@ -1622,7 +1533,7 @@ export function registerPerformanceTools(
1622
1533
  isError: true,
1623
1534
  };
1624
1535
  }
1625
- }
1536
+ },
1626
1537
  );
1627
1538
 
1628
1539
  // Tool 2: get_period_returns
@@ -1633,10 +1544,7 @@ export function registerPerformanceTools(
1633
1544
  "Get P&L breakdown by period (monthly, weekly, or daily) with gross P/L, commissions, and net P/L",
1634
1545
  inputSchema: z.object({
1635
1546
  blockId: z.string().describe("Block folder name"),
1636
- strategy: z
1637
- .string()
1638
- .optional()
1639
- .describe("Filter by strategy name (case-insensitive)"),
1547
+ strategy: z.string().optional().describe("Filter by strategy name (case-insensitive)"),
1640
1548
  period: z
1641
1549
  .enum(["monthly", "weekly", "daily"])
1642
1550
  .default("monthly")
@@ -1647,23 +1555,15 @@ export function registerPerformanceTools(
1647
1555
  .describe("Filter to specific year (optional, alternative to dateRange)"),
1648
1556
  dateRange: z
1649
1557
  .object({
1650
- from: z
1651
- .string()
1652
- .optional()
1653
- .describe("Start date YYYY-MM-DD (inclusive)"),
1654
- to: z
1655
- .string()
1656
- .optional()
1657
- .describe("End date YYYY-MM-DD (inclusive)"),
1558
+ from: z.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
1559
+ to: z.string().optional().describe("End date YYYY-MM-DD (inclusive)"),
1658
1560
  })
1659
1561
  .optional()
1660
1562
  .describe("Filter trades to date range (takes precedence over year)"),
1661
1563
  normalizeTo1Lot: z
1662
1564
  .boolean()
1663
1565
  .default(false)
1664
- .describe(
1665
- "Normalize all trades to 1 contract for fair comparison"
1666
- ),
1566
+ .describe("Normalize all trades to 1 contract for fair comparison"),
1667
1567
  }),
1668
1568
  },
1669
1569
  async ({ blockId, strategy, period, year, dateRange, normalizeTo1Lot }) => {
@@ -1678,9 +1578,7 @@ export function registerPerformanceTools(
1678
1578
  if (dateRange) {
1679
1579
  trades = filterByDateRange(trades, dateRange.from, dateRange.to);
1680
1580
  } else if (year !== undefined) {
1681
- trades = trades.filter(
1682
- (t) => new Date(t.dateOpened).getFullYear() === year
1683
- );
1581
+ trades = trades.filter((t) => new Date(t.dateOpened).getFullYear() === year);
1684
1582
  }
1685
1583
 
1686
1584
  // Apply normalization if requested
@@ -1740,8 +1638,7 @@ export function registerPerformanceTools(
1740
1638
  };
1741
1639
 
1742
1640
  const totalCommissions =
1743
- (trade.openingCommissionsFees ?? 0) +
1744
- (trade.closingCommissionsFees ?? 0);
1641
+ (trade.openingCommissionsFees ?? 0) + (trade.closingCommissionsFees ?? 0);
1745
1642
 
1746
1643
  existing.grossPl += trade.pl;
1747
1644
  existing.commissions += totalCommissions;
@@ -1770,9 +1667,7 @@ export function registerPerformanceTools(
1770
1667
  const filters: string[] = [];
1771
1668
  if (strategy) filters.push(`strategy=${strategy}`);
1772
1669
  if (dateRange?.from || dateRange?.to) {
1773
- filters.push(
1774
- `date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
1775
- );
1670
+ filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
1776
1671
  } else if (year !== undefined) {
1777
1672
  filters.push(`year=${year}`);
1778
1673
  }
@@ -1807,7 +1702,7 @@ export function registerPerformanceTools(
1807
1702
  isError: true,
1808
1703
  };
1809
1704
  }
1810
- }
1705
+ },
1811
1706
  );
1812
1707
 
1813
1708
  // Tool 3: compare_backtest_to_actual
@@ -1822,24 +1717,18 @@ export function registerPerformanceTools(
1822
1717
  .string()
1823
1718
  .optional()
1824
1719
  .describe(
1825
- "Filter to specific strategy name (matches both backtest and actual by strategy)"
1720
+ "Filter to specific strategy name (matches both backtest and actual by strategy)",
1826
1721
  ),
1827
1722
  scaling: z
1828
1723
  .enum(["raw", "perContract", "toReported"])
1829
1724
  .default("raw")
1830
1725
  .describe(
1831
- "Scaling mode: 'raw' (no scaling), 'perContract' (divide by contracts for per-lot comparison), 'toReported' (scale backtest DOWN to match actual contract count)"
1726
+ "Scaling mode: 'raw' (no scaling), 'perContract' (divide by contracts for per-lot comparison), 'toReported' (scale backtest DOWN to match actual contract count)",
1832
1727
  ),
1833
1728
  dateRange: z
1834
1729
  .object({
1835
- from: z
1836
- .string()
1837
- .optional()
1838
- .describe("Start date YYYY-MM-DD (inclusive)"),
1839
- to: z
1840
- .string()
1841
- .optional()
1842
- .describe("End date YYYY-MM-DD (inclusive)"),
1730
+ from: z.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
1731
+ to: z.string().optional().describe("End date YYYY-MM-DD (inclusive)"),
1843
1732
  })
1844
1733
  .optional()
1845
1734
  .describe("Filter trades to date range"),
@@ -1847,35 +1736,41 @@ export function registerPerformanceTools(
1847
1736
  .boolean()
1848
1737
  .default(false)
1849
1738
  .describe(
1850
- "Only include trades where both backtest and actual exist on the same date (excludes unmatched rows from output and totals)"
1739
+ "Only include trades where both backtest and actual exist on the same date (excludes unmatched rows from output and totals)",
1851
1740
  ),
1852
1741
  detailLevel: z
1853
1742
  .enum(["summary", "trades"])
1854
1743
  .default("summary")
1855
1744
  .describe(
1856
- "'summary' (default): aggregate by date+strategy. 'trades': individual trade comparison with field-by-field differences"
1745
+ "'summary' (default): aggregate by date+strategy. 'trades': individual trade comparison with field-by-field differences",
1857
1746
  ),
1858
1747
  outliersOnly: z
1859
1748
  .boolean()
1860
1749
  .default(false)
1861
- .describe(
1862
- "Only return high-slippage outliers (trades exceeding z-score threshold)"
1863
- ),
1750
+ .describe("Only return high-slippage outliers (trades exceeding z-score threshold)"),
1864
1751
  outliersThreshold: z
1865
1752
  .number()
1866
1753
  .default(2)
1867
- .describe(
1868
- "Z-score threshold for outlier detection (default: 2 = ~95% confidence)"
1869
- ),
1754
+ .describe("Z-score threshold for outlier detection (default: 2 = ~95% confidence)"),
1870
1755
  groupBy: z
1871
1756
  .enum(["none", "strategy", "date", "week", "month"])
1872
1757
  .default("none")
1873
1758
  .describe(
1874
- "Group results: 'none' (flat list), 'strategy', 'date' (daily), 'week', 'month'"
1759
+ "Group results: 'none' (flat list), 'strategy', 'date' (daily), 'week', 'month'",
1875
1760
  ),
1876
1761
  }),
1877
1762
  },
1878
- async ({ blockId, strategy, scaling, dateRange, matchedOnly, detailLevel, outliersOnly, outliersThreshold, groupBy }) => {
1763
+ async ({
1764
+ blockId,
1765
+ strategy,
1766
+ scaling,
1767
+ dateRange,
1768
+ matchedOnly,
1769
+ detailLevel,
1770
+ outliersOnly,
1771
+ outliersThreshold,
1772
+ groupBy,
1773
+ }) => {
1879
1774
  try {
1880
1775
  const block = await loadBlock(baseDir, blockId);
1881
1776
  let backtestTrades = block.trades;
@@ -1932,12 +1827,12 @@ export function registerPerformanceTools(
1932
1827
  // when no explicit dateRange is provided
1933
1828
  let autoFilterApplied = false;
1934
1829
  if (!dateRange) {
1935
- const actualDates = actualTrades.map(t => formatDateKey(new Date(t.dateOpened)));
1830
+ const actualDates = actualTrades.map((t) => formatDateKey(new Date(t.dateOpened)));
1936
1831
  if (actualDates.length > 0) {
1937
- const minActualDate = actualDates.reduce((a, b) => a < b ? a : b);
1938
- const maxActualDate = actualDates.reduce((a, b) => a > b ? a : b);
1832
+ const minActualDate = actualDates.reduce((a, b) => (a < b ? a : b));
1833
+ const maxActualDate = actualDates.reduce((a, b) => (a > b ? a : b));
1939
1834
  const beforeCount = backtestTrades.length;
1940
- backtestTrades = backtestTrades.filter(t => {
1835
+ backtestTrades = backtestTrades.filter((t) => {
1941
1836
  const d = formatDateKey(new Date(t.dateOpened));
1942
1837
  return d >= minActualDate && d <= maxActualDate;
1943
1838
  });
@@ -1949,7 +1844,7 @@ export function registerPerformanceTools(
1949
1844
  const getGroupKey = (
1950
1845
  dateStr: string,
1951
1846
  strategyName: string,
1952
- groupByMode: typeof groupBy
1847
+ groupByMode: typeof groupBy,
1953
1848
  ): string => {
1954
1849
  if (groupByMode === "strategy") {
1955
1850
  return strategyName;
@@ -2053,13 +1948,18 @@ export function registerPerformanceTools(
2053
1948
  const btContracts = btTrade.numContracts;
2054
1949
  const actualContracts = actualTrade.numContracts;
2055
1950
  const { scaledBtPl, scaledActualPl: actualPl } = calculateScaledPl(
2056
- btTrade.pl, actualTrade.pl,
2057
- btContracts, actualContracts,
2058
- scaling
1951
+ btTrade.pl,
1952
+ actualTrade.pl,
1953
+ btContracts,
1954
+ actualContracts,
1955
+ scaling,
2059
1956
  );
2060
- const scalingFactor = scaling === "toReported" && btContracts > 0 && actualContracts > 0
2061
- ? actualContracts / btContracts
2062
- : scaling === "toReported" && btContracts === 0 ? 0 : 1;
1957
+ const scalingFactor =
1958
+ scaling === "toReported" && btContracts > 0 && actualContracts > 0
1959
+ ? actualContracts / btContracts
1960
+ : scaling === "toReported" && btContracts === 0
1961
+ ? 0
1962
+ : 1;
2063
1963
 
2064
1964
  const slippage = actualPl - scaledBtPl;
2065
1965
  const slippagePercent =
@@ -2269,13 +2169,16 @@ export function registerPerformanceTools(
2269
2169
  processedActual.add(key);
2270
2170
 
2271
2171
  const { scaledBtPl, scaledActualPl } = calculateScaledPl(
2272
- btData.totalPl, actualData.totalPl,
2273
- btData.contracts, actualData.contracts,
2274
- scaling
2172
+ btData.totalPl,
2173
+ actualData.totalPl,
2174
+ btData.contracts,
2175
+ actualData.contracts,
2176
+ scaling,
2275
2177
  );
2276
- const scalingFactor = scaling === "toReported" && btData.contracts > 0 && actualData.contracts > 0
2277
- ? actualData.contracts / btData.contracts
2278
- : 1;
2178
+ const scalingFactor =
2179
+ scaling === "toReported" && btData.contracts > 0 && actualData.contracts > 0
2180
+ ? actualData.contracts / btData.contracts
2181
+ : 1;
2279
2182
 
2280
2183
  const slippage = scaledActualPl - scaledBtPl;
2281
2184
  const slippagePercent =
@@ -2378,10 +2281,8 @@ export function registerPerformanceTools(
2378
2281
  const meanSlippage =
2379
2282
  slippageValues.reduce((sum, v) => sum + v, 0) / slippageValues.length;
2380
2283
  const variance =
2381
- slippageValues.reduce(
2382
- (sum, v) => sum + Math.pow(v - meanSlippage, 2),
2383
- 0
2384
- ) / slippageValues.length;
2284
+ slippageValues.reduce((sum, v) => sum + Math.pow(v - meanSlippage, 2), 0) /
2285
+ slippageValues.length;
2385
2286
  const stdDevSlippage = Math.sqrt(variance);
2386
2287
 
2387
2288
  // Guard: skip if all values are essentially the same
@@ -2389,8 +2290,7 @@ export function registerPerformanceTools(
2389
2290
  // Calculate z-scores and flag outliers
2390
2291
  for (const comparison of comparisons) {
2391
2292
  if (comparison.matched) {
2392
- const zScore =
2393
- (comparison.slippage - meanSlippage) / stdDevSlippage;
2293
+ const zScore = (comparison.slippage - meanSlippage) / stdDevSlippage;
2394
2294
  comparison.zScore = zScore;
2395
2295
 
2396
2296
  if (Math.abs(zScore) >= outliersThreshold) {
@@ -2407,10 +2307,7 @@ export function registerPerformanceTools(
2407
2307
  }
2408
2308
 
2409
2309
  const outliers = comparisons.filter((c) => c.isOutlier);
2410
- const outlierTotalSlippage = outliers.reduce(
2411
- (sum, c) => sum + c.slippage,
2412
- 0
2413
- );
2310
+ const outlierTotalSlippage = outliers.reduce((sum, c) => sum + c.slippage, 0);
2414
2311
 
2415
2312
  outlierStats = {
2416
2313
  meanSlippage,
@@ -2422,43 +2319,46 @@ export function registerPerformanceTools(
2422
2319
  ? (outliers.length / matchedComparisons.length) * 100
2423
2320
  : 0,
2424
2321
  outlierTotalSlippage,
2425
- outlierAvgSlippage:
2426
- outliers.length > 0 ? outlierTotalSlippage / outliers.length : 0,
2322
+ outlierAvgSlippage: outliers.length > 0 ? outlierTotalSlippage / outliers.length : 0,
2427
2323
  };
2428
2324
  }
2429
2325
  }
2430
2326
 
2431
2327
  // Build unmatched summaries before filtering
2432
2328
  const unmatchedBacktestEntries = comparisons.filter(
2433
- (c) => !c.matched && c.backtestPl !== 0
2329
+ (c) => !c.matched && c.backtestPl !== 0,
2434
2330
  );
2435
- const unmatchedActualEntries = comparisons.filter(
2436
- (c) => !c.matched && c.actualPl !== 0
2437
- );
2438
-
2439
- const unmatchedBacktestSummary = unmatchedBacktestEntries.length > 0
2440
- ? {
2441
- count: unmatchedBacktestEntries.length,
2442
- dateRange: {
2443
- from: unmatchedBacktestEntries.reduce((a, b) => a.date < b.date ? a : b).date,
2444
- to: unmatchedBacktestEntries.reduce((a, b) => a.date > b.date ? a : b).date,
2445
- },
2446
- totalPl: unmatchedBacktestEntries.reduce((sum, c) => sum + c.backtestPl, 0),
2447
- strategies: Array.from(new Set(unmatchedBacktestEntries.map(c => c.strategy))).sort(),
2448
- }
2449
- : null;
2331
+ const unmatchedActualEntries = comparisons.filter((c) => !c.matched && c.actualPl !== 0);
2332
+
2333
+ const unmatchedBacktestSummary =
2334
+ unmatchedBacktestEntries.length > 0
2335
+ ? {
2336
+ count: unmatchedBacktestEntries.length,
2337
+ dateRange: {
2338
+ from: unmatchedBacktestEntries.reduce((a, b) => (a.date < b.date ? a : b)).date,
2339
+ to: unmatchedBacktestEntries.reduce((a, b) => (a.date > b.date ? a : b)).date,
2340
+ },
2341
+ totalPl: unmatchedBacktestEntries.reduce((sum, c) => sum + c.backtestPl, 0),
2342
+ strategies: Array.from(
2343
+ new Set(unmatchedBacktestEntries.map((c) => c.strategy)),
2344
+ ).sort(),
2345
+ }
2346
+ : null;
2450
2347
 
2451
- const unmatchedActualSummary = unmatchedActualEntries.length > 0
2452
- ? {
2453
- count: unmatchedActualEntries.length,
2454
- dateRange: {
2455
- from: unmatchedActualEntries.reduce((a, b) => a.date < b.date ? a : b).date,
2456
- to: unmatchedActualEntries.reduce((a, b) => a.date > b.date ? a : b).date,
2457
- },
2458
- totalPl: unmatchedActualEntries.reduce((sum, c) => sum + c.actualPl, 0),
2459
- strategies: Array.from(new Set(unmatchedActualEntries.map(c => c.strategy))).sort(),
2460
- }
2461
- : null;
2348
+ const unmatchedActualSummary =
2349
+ unmatchedActualEntries.length > 0
2350
+ ? {
2351
+ count: unmatchedActualEntries.length,
2352
+ dateRange: {
2353
+ from: unmatchedActualEntries.reduce((a, b) => (a.date < b.date ? a : b)).date,
2354
+ to: unmatchedActualEntries.reduce((a, b) => (a.date > b.date ? a : b)).date,
2355
+ },
2356
+ totalPl: unmatchedActualEntries.reduce((sum, c) => sum + c.actualPl, 0),
2357
+ strategies: Array.from(
2358
+ new Set(unmatchedActualEntries.map((c) => c.strategy)),
2359
+ ).sort(),
2360
+ }
2361
+ : null;
2462
2362
 
2463
2363
  // Apply matchedOnly filter to the primary output set
2464
2364
  let outputComparisons = matchedOnly
@@ -2471,9 +2371,7 @@ export function registerPerformanceTools(
2471
2371
  }
2472
2372
 
2473
2373
  // Sort by absolute slippage (worst first) to surface problem areas
2474
- outputComparisons.sort(
2475
- (a, b) => Math.abs(b.slippage) - Math.abs(a.slippage)
2476
- );
2374
+ outputComparisons.sort((a, b) => Math.abs(b.slippage) - Math.abs(a.slippage));
2477
2375
 
2478
2376
  // Apply grouping if requested
2479
2377
  let groups: GroupedResult[] | null = null;
@@ -2489,23 +2387,15 @@ export function registerPerformanceTools(
2489
2387
  groups = Array.from(groupMap.entries())
2490
2388
  .map(([groupKey, groupComparisons]) => {
2491
2389
  const matchedInGroup = groupComparisons.filter((c) => c.matched);
2492
- const totalSlippage = groupComparisons.reduce(
2493
- (sum, c) => sum + c.slippage,
2494
- 0
2495
- );
2496
- const outlierCount = groupComparisons.filter(
2497
- (c) => c.isOutlier
2498
- ).length;
2390
+ const totalSlippage = groupComparisons.reduce((sum, c) => sum + c.slippage, 0);
2391
+ const outlierCount = groupComparisons.filter((c) => c.isOutlier).length;
2499
2392
 
2500
2393
  return {
2501
2394
  groupKey,
2502
2395
  count: groupComparisons.length,
2503
2396
  matchedCount: matchedInGroup.length,
2504
2397
  totalSlippage,
2505
- avgSlippage:
2506
- matchedInGroup.length > 0
2507
- ? totalSlippage / matchedInGroup.length
2508
- : 0,
2398
+ avgSlippage: matchedInGroup.length > 0 ? totalSlippage / matchedInGroup.length : 0,
2509
2399
  outlierCount,
2510
2400
  comparisons: groupComparisons,
2511
2401
  };
@@ -2521,40 +2411,35 @@ export function registerPerformanceTools(
2521
2411
  const matchedForSummary = outputComparisons.filter((c) => c.matched);
2522
2412
  const totalBacktestPl = comparisonsForTotals.reduce(
2523
2413
  (sum, c) => sum + c.scaledBacktestPl,
2524
- 0
2414
+ 0,
2525
2415
  );
2526
2416
  const totalActualPl = comparisonsForTotals.reduce(
2527
- (sum, c) => sum + (scaling === "perContract" && c.actualContracts > 0
2528
- ? c.actualPl / c.actualContracts
2529
- : c.actualPl),
2530
- 0
2417
+ (sum, c) =>
2418
+ sum +
2419
+ (scaling === "perContract" && c.actualContracts > 0
2420
+ ? c.actualPl / c.actualContracts
2421
+ : c.actualPl),
2422
+ 0,
2531
2423
  );
2532
2424
  const totalSlippage = totalActualPl - totalBacktestPl;
2533
2425
  const avgSlippage =
2534
2426
  matchedForSummary.length > 0
2535
- ? matchedForSummary.reduce((sum, c) => sum + c.slippage, 0) /
2536
- matchedForSummary.length
2427
+ ? matchedForSummary.reduce((sum, c) => sum + c.slippage, 0) / matchedForSummary.length
2537
2428
  : 0;
2538
2429
  const avgSlippagePercent =
2539
- totalBacktestPl !== 0
2540
- ? (totalSlippage / Math.abs(totalBacktestPl)) * 100
2541
- : null;
2430
+ totalBacktestPl !== 0 ? (totalSlippage / Math.abs(totalBacktestPl)) * 100 : null;
2542
2431
 
2543
2432
  // Get unique strategies
2544
2433
  const backtestStrategies = Array.from(
2545
- new Set(backtestTrades.map((t) => t.strategy))
2546
- ).sort();
2547
- const actualStrategies = Array.from(
2548
- new Set(actualTrades.map((t) => t.strategy))
2434
+ new Set(backtestTrades.map((t) => t.strategy)),
2549
2435
  ).sort();
2436
+ const actualStrategies = Array.from(new Set(actualTrades.map((t) => t.strategy))).sort();
2550
2437
 
2551
2438
  // Brief summary for user display
2552
2439
  const filters: string[] = [];
2553
2440
  if (strategy) filters.push(`strategy=${strategy}`);
2554
2441
  if (dateRange?.from || dateRange?.to) {
2555
- filters.push(
2556
- `date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
2557
- );
2442
+ filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
2558
2443
  }
2559
2444
  if (autoFilterApplied) filters.push("auto-date-overlap");
2560
2445
  if (matchedOnly) filters.push("matched-only");
@@ -2568,9 +2453,7 @@ export function registerPerformanceTools(
2568
2453
  ? `${formatPercent(avgSlippagePercent)} slippage`
2569
2454
  : "N/A slippage";
2570
2455
  const outlierDisplay =
2571
- outlierStats !== null
2572
- ? ` | ${outlierStats.outlierCount} outliers`
2573
- : "";
2456
+ outlierStats !== null ? ` | ${outlierStats.outlierCount} outliers` : "";
2574
2457
  const summary = `Comparison: ${blockId}${filterStr} | ${scaling} scaling | ${matchedForSummary.length}/${outputComparisons.length} matched | ${slippageDisplay}${outlierDisplay}`;
2575
2458
 
2576
2459
  // Build structured data for Claude reasoning
@@ -2627,6 +2510,6 @@ export function registerPerformanceTools(
2627
2510
  isError: true,
2628
2511
  };
2629
2512
  }
2630
- }
2513
+ },
2631
2514
  );
2632
2515
  }