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
@@ -22,10 +22,7 @@ import { withSyncedBlock } from "../middleware/sync-middleware.ts";
22
22
  /**
23
23
  * Register analysis block tools
24
24
  */
25
- export function registerAnalysisBlockTools(
26
- server: McpServer,
27
- baseDir: string
28
- ): void {
25
+ export function registerAnalysisBlockTools(server: McpServer, baseDir: string): void {
29
26
  const calculator = new PortfolioStatsCalculator();
30
27
 
31
28
  // Tool 7: stress_test
@@ -40,7 +37,7 @@ export function registerAnalysisBlockTools(
40
37
  .array(z.string())
41
38
  .optional()
42
39
  .describe(
43
- "Specific scenario names to test (e.g., 'covid_crash', 'bear_2022'). If omitted, runs all built-in scenarios."
40
+ "Specific scenario names to test (e.g., 'covid_crash', 'bear_2022'). If omitted, runs all built-in scenarios.",
44
41
  ),
45
42
  customScenarios: z
46
43
  .array(
@@ -48,7 +45,7 @@ export function registerAnalysisBlockTools(
48
45
  name: z.string().describe("Custom scenario name"),
49
46
  startDate: z.string().describe("Start date (YYYY-MM-DD)"),
50
47
  endDate: z.string().describe("End date (YYYY-MM-DD)"),
51
- })
48
+ }),
52
49
  )
53
50
  .optional()
54
51
  .describe("User-defined scenarios with custom date ranges"),
@@ -57,21 +54,18 @@ export function registerAnalysisBlockTools(
57
54
  .optional()
58
55
  .default(false)
59
56
  .describe(
60
- "Include scenarios with no trades in the results. Default false - only shows scenarios with data coverage."
57
+ "Include scenarios with no trades in the results. Default false - only shows scenarios with data coverage.",
61
58
  ),
62
59
  }),
63
60
  },
64
- withSyncedBlock(
65
- baseDir,
66
- async ({ blockId, scenarios, customScenarios, includeEmpty }) => {
67
- try {
68
- const block = await loadBlock(baseDir, blockId);
61
+ withSyncedBlock(baseDir, async ({ blockId, scenarios, customScenarios, includeEmpty }) => {
62
+ try {
63
+ const block = await loadBlock(baseDir, blockId);
69
64
  const trades = block.trades;
70
65
 
71
66
  // Get portfolio date range for context and pre-filtering
72
67
  const sortedTrades = [...trades].sort(
73
- (a, b) =>
74
- new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
68
+ (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
75
69
  );
76
70
  const portfolioStartDate = sortedTrades[0]?.dateOpened
77
71
  ? new Date(sortedTrades[0].dateOpened).toISOString().split("T")[0]
@@ -82,15 +76,19 @@ export function registerAnalysisBlockTools(
82
76
  : null;
83
77
 
84
78
  // Build list of scenarios to run
85
- const scenariosToRun: Array<{ name: string; startDate: string; endDate: string; description: string; isCustom: boolean }> = [];
79
+ const scenariosToRun: Array<{
80
+ name: string;
81
+ startDate: string;
82
+ endDate: string;
83
+ description: string;
84
+ isCustom: boolean;
85
+ }> = [];
86
86
  const preFilteredScenarioNames: string[] = [];
87
87
 
88
88
  // Add built-in scenarios
89
89
  if (scenarios && scenarios.length > 0) {
90
90
  // Validate requested scenarios exist
91
- const invalidScenarios = scenarios.filter(
92
- (s) => !STRESS_SCENARIOS[s]
93
- );
91
+ const invalidScenarios = scenarios.filter((s) => !STRESS_SCENARIOS[s]);
94
92
  if (invalidScenarios.length > 0) {
95
93
  return {
96
94
  content: [
@@ -152,8 +150,23 @@ export function registerAnalysisBlockTools(
152
150
  }
153
151
 
154
152
  // Calculate stats for each scenario
155
- type ScenarioStats = { netPl: number; winRate: number; maxDrawdown: number; profitFactor: number | null; avgWin: number | null; avgLoss: number | null };
156
- const scenarioResults: Array<{ name: string; description: string; dateRange: { start: string; end: string }; tradeCount: number; stats: ScenarioStats | null; isCustom: boolean; noCoverage?: boolean }> = [];
153
+ type ScenarioStats = {
154
+ netPl: number;
155
+ winRate: number;
156
+ maxDrawdown: number;
157
+ profitFactor: number | null;
158
+ avgWin: number | null;
159
+ avgLoss: number | null;
160
+ };
161
+ const scenarioResults: Array<{
162
+ name: string;
163
+ description: string;
164
+ dateRange: { start: string; end: string };
165
+ tradeCount: number;
166
+ stats: ScenarioStats | null;
167
+ isCustom: boolean;
168
+ noCoverage?: boolean;
169
+ }> = [];
157
170
 
158
171
  let worstScenario: { name: string; netPl: number } | null = null;
159
172
  let bestScenario: { name: string; netPl: number } | null = null;
@@ -163,11 +176,7 @@ export function registerAnalysisBlockTools(
163
176
 
164
177
  for (const scenario of scenariosToRun) {
165
178
  // Filter trades to scenario date range
166
- const scenarioTrades = filterByDateRange(
167
- trades,
168
- scenario.startDate,
169
- scenario.endDate
170
- );
179
+ const scenarioTrades = filterByDateRange(trades, scenario.startDate, scenario.endDate);
171
180
 
172
181
  if (scenarioTrades.length === 0) {
173
182
  // Genuine coverage gap (had date overlap but zero trades)
@@ -189,7 +198,7 @@ export function registerAnalysisBlockTools(
189
198
  const stats = calculator.calculatePortfolioStats(
190
199
  scenarioTrades,
191
200
  undefined, // No daily logs
192
- true // Force trade-based calculations
201
+ true, // Force trade-based calculations
193
202
  );
194
203
 
195
204
  scenarioResults.push({
@@ -226,7 +235,9 @@ export function registerAnalysisBlockTools(
226
235
  scenariosWithTrades,
227
236
  scenariosSkipped,
228
237
  ...(skippedScenarioNames.length > 0 ? { skippedScenarios: skippedScenarioNames } : {}),
229
- ...(preFilteredScenarioNames.length > 0 ? { preFilteredScenarios: preFilteredScenarioNames } : {}),
238
+ ...(preFilteredScenarioNames.length > 0
239
+ ? { preFilteredScenarios: preFilteredScenarioNames }
240
+ : {}),
230
241
  worstScenario: worstScenario?.name ?? null,
231
242
  bestScenario: bestScenario?.name ?? null,
232
243
  portfolioDateRange: {
@@ -237,9 +248,7 @@ export function registerAnalysisBlockTools(
237
248
 
238
249
  // Brief summary for user display
239
250
  const skippedNote =
240
- scenariosSkipped > 0
241
- ? ` (${scenariosSkipped} skipped - no data coverage)`
242
- : "";
251
+ scenariosSkipped > 0 ? ` (${scenariosSkipped} skipped - no data coverage)` : "";
243
252
  const preFilterNote =
244
253
  preFilteredScenarioNames.length > 0
245
254
  ? ` (${preFilteredScenarioNames.length} excluded - outside portfolio date range)`
@@ -254,20 +263,19 @@ export function registerAnalysisBlockTools(
254
263
  availableBuiltInScenarios: Object.keys(STRESS_SCENARIOS),
255
264
  };
256
265
 
257
- return createToolOutput(summary, structuredData);
258
- } catch (error) {
259
- return {
260
- content: [
261
- {
262
- type: "text",
263
- text: `Error running stress test: ${(error as Error).message}`,
264
- },
265
- ],
266
- isError: true,
267
- };
268
- }
266
+ return createToolOutput(summary, structuredData);
267
+ } catch (error) {
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: `Error running stress test: ${(error as Error).message}`,
273
+ },
274
+ ],
275
+ isError: true,
276
+ };
269
277
  }
270
- )
278
+ }),
271
279
  );
272
280
 
273
281
  // Tool 8: drawdown_attribution
@@ -281,9 +289,7 @@ export function registerAnalysisBlockTools(
281
289
  strategy: z
282
290
  .string()
283
291
  .optional()
284
- .describe(
285
- "Optional: Filter to specific strategy before calculating drawdown"
286
- ),
292
+ .describe("Optional: Filter to specific strategy before calculating drawdown"),
287
293
  topN: z
288
294
  .number()
289
295
  .int()
@@ -328,15 +334,12 @@ export function registerAnalysisBlockTools(
328
334
  // Build equity curve from trades
329
335
  // Initial capital = first trade's fundsAtClose - pl
330
336
  const firstTrade = sortedTrades[0];
331
- const initialCapital =
332
- (firstTrade.fundsAtClose ?? 10000) - firstTrade.pl;
337
+ const initialCapital = (firstTrade.fundsAtClose ?? 10000) - firstTrade.pl;
333
338
 
334
339
  // Track peak equity and drawdown
335
340
  let equity = initialCapital;
336
341
  let peakEquity = initialCapital;
337
- let peakDate: Date = new Date(
338
- firstTrade.dateClosed ?? firstTrade.dateOpened
339
- );
342
+ let peakDate: Date = new Date(firstTrade.dateClosed ?? firstTrade.dateOpened);
340
343
  let maxDrawdown = 0;
341
344
  let maxDrawdownPct = 0;
342
345
  let troughDate: Date | null = null;
@@ -363,8 +366,7 @@ export function registerAnalysisBlockTools(
363
366
 
364
367
  // Calculate current drawdown from peak
365
368
  const drawdown = peakEquity - equity;
366
- const drawdownPct =
367
- peakEquity > 0 ? (drawdown / peakEquity) * 100 : 0;
369
+ const drawdownPct = peakEquity > 0 ? (drawdown / peakEquity) * 100 : 0;
368
370
 
369
371
  equityPoints.push({
370
372
  date: closeDate,
@@ -404,7 +406,10 @@ export function registerAnalysisBlockTools(
404
406
  });
405
407
 
406
408
  // Group trades by strategy and calculate attribution
407
- const strategyPl = new Map<string, { pl: number; trades: number; wins: number; losses: number }>();
409
+ const strategyPl = new Map<
410
+ string,
411
+ { pl: number; trades: number; wins: number; losses: number }
412
+ >();
408
413
 
409
414
  let totalLossDuringDrawdown = 0;
410
415
 
@@ -429,10 +434,18 @@ export function registerAnalysisBlockTools(
429
434
  // Contribution %: strategy's P/L as % of total loss (most negative = highest contribution)
430
435
  const attribution = Array.from(strategyPl.entries())
431
436
  .map(([strategyName, data]) => ({
432
- strategy: strategyName, pl: data.pl, trades: data.trades, wins: data.wins, losses: data.losses,
433
- contributionPct: totalLossDuringDrawdown !== 0 ? Math.abs((data.pl / totalLossDuringDrawdown) * 100) : 0,
437
+ strategy: strategyName,
438
+ pl: data.pl,
439
+ trades: data.trades,
440
+ wins: data.wins,
441
+ losses: data.losses,
442
+ contributionPct:
443
+ totalLossDuringDrawdown !== 0
444
+ ? Math.abs((data.pl / totalLossDuringDrawdown) * 100)
445
+ : 0,
434
446
  }))
435
- .sort((a, b) => a.pl - b.pl).slice(0, topN);
447
+ .sort((a, b) => a.pl - b.pl)
448
+ .slice(0, topN);
436
449
 
437
450
  // Calculate duration in days
438
451
  const durationMs = troughDate.getTime() - drawdownPeakDate.getTime();
@@ -479,7 +492,7 @@ export function registerAnalysisBlockTools(
479
492
  isError: true,
480
493
  };
481
494
  }
482
- })
495
+ }),
483
496
  );
484
497
 
485
498
  // Tool 9: marginal_contribution
@@ -494,7 +507,7 @@ export function registerAnalysisBlockTools(
494
507
  .string()
495
508
  .optional()
496
509
  .describe(
497
- "Calculate for specific strategy only. If omitted, calculates for all strategies."
510
+ "Calculate for specific strategy only. If omitted, calculates for all strategies.",
498
511
  ),
499
512
  topN: z
500
513
  .number()
@@ -503,7 +516,7 @@ export function registerAnalysisBlockTools(
503
516
  .max(50)
504
517
  .default(5)
505
518
  .describe(
506
- "Number of top contributors to return when targetStrategy is omitted (default: 5)"
519
+ "Number of top contributors to return when targetStrategy is omitted (default: 5)",
507
520
  ),
508
521
  }),
509
522
  },
@@ -524,14 +537,12 @@ export function registerAnalysisBlockTools(
524
537
  }
525
538
 
526
539
  // Get unique strategies
527
- const strategies = Array.from(
528
- new Set(trades.map((t) => t.strategy))
529
- ).sort();
540
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
530
541
 
531
542
  // Validate targetStrategy if provided
532
543
  if (targetStrategy) {
533
544
  const matchedStrategy = strategies.find(
534
- (s) => s.toLowerCase() === targetStrategy.toLowerCase()
545
+ (s) => s.toLowerCase() === targetStrategy.toLowerCase(),
535
546
  );
536
547
  if (!matchedStrategy) {
537
548
  return {
@@ -549,11 +560,9 @@ export function registerAnalysisBlockTools(
549
560
  // Edge case: single strategy portfolio
550
561
  if (strategies.length === 1) {
551
562
  // Use daily logs for baseline when available (consistent with get_statistics)
552
- const dailyLogs = block.dailyLogs && block.dailyLogs.length > 0 ? block.dailyLogs : undefined;
553
- const baselineStats = calculator.calculatePortfolioStats(
554
- trades,
555
- dailyLogs,
556
- );
563
+ const dailyLogs =
564
+ block.dailyLogs && block.dailyLogs.length > 0 ? block.dailyLogs : undefined;
565
+ const baselineStats = calculator.calculatePortfolioStats(trades, dailyLogs);
557
566
 
558
567
  const summary = `Marginal Contribution: ${blockId} | Single strategy portfolio - cannot calculate marginal contribution`;
559
568
 
@@ -587,33 +596,34 @@ export function registerAnalysisBlockTools(
587
596
 
588
597
  // Calculate baseline portfolio metrics using ALL trades
589
598
  // Use daily logs for baseline when available (consistent with get_statistics)
590
- const dailyLogs = block.dailyLogs && block.dailyLogs.length > 0 ? block.dailyLogs : undefined;
591
- const baselineStats = calculator.calculatePortfolioStats(
592
- trades,
593
- dailyLogs,
594
- );
599
+ const dailyLogs =
600
+ block.dailyLogs && block.dailyLogs.length > 0 ? block.dailyLogs : undefined;
601
+ const baselineStats = calculator.calculatePortfolioStats(trades, dailyLogs);
595
602
 
596
603
  // Determine which strategies to analyze
597
604
  const strategiesToAnalyze = targetStrategy
598
- ? strategies.filter(
599
- (s) => s.toLowerCase() === targetStrategy.toLowerCase()
600
- )
605
+ ? strategies.filter((s) => s.toLowerCase() === targetStrategy.toLowerCase())
601
606
  : strategies;
602
607
 
603
608
  // Calculate marginal contribution for each strategy
604
609
  // NOTE: Baseline uses daily-log Sharpe (matching get_statistics), but "without" uses
605
610
  // trade-based Sharpe because daily logs include the removed strategy's impact.
606
611
  // This means marginal deltas are mixed-basis, but the baseline values match get_statistics.
607
- type Contribution = { strategy: string; trades: number; marginalSharpe: number | null; marginalSortino: number | null };
612
+ type Contribution = {
613
+ strategy: string;
614
+ trades: number;
615
+ marginalSharpe: number | null;
616
+ marginalSortino: number | null;
617
+ };
608
618
  const contributions: Contribution[] = [];
609
619
 
610
620
  for (const strategy of strategiesToAnalyze) {
611
621
  // Filter OUT this strategy's trades (portfolio WITHOUT this strategy)
612
622
  const tradesWithout = trades.filter(
613
- (t) => t.strategy.toLowerCase() !== strategy.toLowerCase()
623
+ (t) => t.strategy.toLowerCase() !== strategy.toLowerCase(),
614
624
  );
615
625
  const strategyTrades = trades.filter(
616
- (t) => t.strategy.toLowerCase() === strategy.toLowerCase()
626
+ (t) => t.strategy.toLowerCase() === strategy.toLowerCase(),
617
627
  );
618
628
 
619
629
  // Edge case: removing this strategy leaves nothing
@@ -632,14 +642,20 @@ export function registerAnalysisBlockTools(
632
642
  const withoutStats = calculator.calculatePortfolioStats(
633
643
  tradesWithout,
634
644
  undefined,
635
- true // Force trade-based - daily logs include the removed strategy's impact
645
+ true, // Force trade-based - daily logs include the removed strategy's impact
636
646
  );
637
647
 
638
648
  // Marginal contribution = baseline - without (positive = improves, negative = hurts)
639
- const hasValidSharpe = baselineStats.sharpeRatio != null && withoutStats.sharpeRatio != null;
640
- const hasValidSortino = baselineStats.sortinoRatio != null && withoutStats.sortinoRatio != null;
641
- const marginalSharpe = hasValidSharpe ? baselineStats.sharpeRatio! - withoutStats.sharpeRatio! : null;
642
- const marginalSortino = hasValidSortino ? baselineStats.sortinoRatio! - withoutStats.sortinoRatio! : null;
649
+ const hasValidSharpe =
650
+ baselineStats.sharpeRatio != null && withoutStats.sharpeRatio != null;
651
+ const hasValidSortino =
652
+ baselineStats.sortinoRatio != null && withoutStats.sortinoRatio != null;
653
+ const marginalSharpe = hasValidSharpe
654
+ ? baselineStats.sharpeRatio! - withoutStats.sharpeRatio!
655
+ : null;
656
+ const marginalSortino = hasValidSortino
657
+ ? baselineStats.sortinoRatio! - withoutStats.sortinoRatio!
658
+ : null;
643
659
 
644
660
  contributions.push({
645
661
  strategy,
@@ -659,19 +675,22 @@ export function registerAnalysisBlockTools(
659
675
  });
660
676
 
661
677
  // Apply topN limit (only when not filtering by targetStrategy)
662
- const limitedContributions = targetStrategy
663
- ? contributions
664
- : contributions.slice(0, topN);
678
+ const limitedContributions = targetStrategy ? contributions : contributions.slice(0, topN);
665
679
 
666
680
  // Find most and least beneficial
667
681
  const validContributions = contributions.filter((c) => c.marginalSharpe !== null);
668
- const mostBeneficial = validContributions.length > 0
669
- ? { strategy: validContributions[0].strategy, sharpe: validContributions[0].marginalSharpe }
670
- : null;
682
+ const mostBeneficial =
683
+ validContributions.length > 0
684
+ ? {
685
+ strategy: validContributions[0].strategy,
686
+ sharpe: validContributions[0].marginalSharpe,
687
+ }
688
+ : null;
671
689
  const lastValid = validContributions[validContributions.length - 1];
672
- const leastBeneficial = validContributions.length > 0
673
- ? { strategy: lastValid.strategy, sharpe: lastValid.marginalSharpe }
674
- : null;
690
+ const leastBeneficial =
691
+ validContributions.length > 0
692
+ ? { strategy: lastValid.strategy, sharpe: lastValid.marginalSharpe }
693
+ : null;
675
694
 
676
695
  // Build summary line
677
696
  const summaryParts: string[] = [`Marginal Contribution: ${blockId}`];
@@ -680,9 +699,7 @@ export function registerAnalysisBlockTools(
680
699
  mostBeneficial.sharpe >= 0
681
700
  ? `+${formatRatio(mostBeneficial.sharpe)}`
682
701
  : formatRatio(mostBeneficial.sharpe);
683
- summaryParts.push(
684
- `Top: ${mostBeneficial.strategy} (Sharpe ${sharpeStr})`
685
- );
702
+ summaryParts.push(`Top: ${mostBeneficial.strategy} (Sharpe ${sharpeStr})`);
686
703
  }
687
704
  if (
688
705
  leastBeneficial &&
@@ -693,9 +710,7 @@ export function registerAnalysisBlockTools(
693
710
  leastBeneficial.sharpe >= 0
694
711
  ? `+${formatRatio(leastBeneficial.sharpe)}`
695
712
  : formatRatio(leastBeneficial.sharpe);
696
- summaryParts.push(
697
- `Worst: ${leastBeneficial.strategy} (Sharpe ${sharpeStr})`
698
- );
713
+ summaryParts.push(`Worst: ${leastBeneficial.strategy} (Sharpe ${sharpeStr})`);
699
714
  }
700
715
  const summary = summaryParts.join(" | ");
701
716
 
@@ -728,6 +743,6 @@ export function registerAnalysisBlockTools(
728
743
  isError: true,
729
744
  };
730
745
  }
731
- })
746
+ }),
732
747
  );
733
748
  }