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
package/server/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  writeParquetAtomic,
41
41
  writeQuoteMinutesPartition,
42
42
  writeSpotPartition
43
- } from "./chunk-NZO6PT64.js";
43
+ } from "./chunk-4P7D7YZP.js";
44
44
  import {
45
45
  BACHELIER_DTE_THRESHOLD,
46
46
  MassiveProvider,
@@ -54,7 +54,7 @@ import {
54
54
  getProvider,
55
55
  markPrice,
56
56
  parseLegsString
57
- } from "./chunk-PNKG7RY7.js";
57
+ } from "./chunk-ZBJCF4ZG.js";
58
58
  import {
59
59
  PortfolioStatsCalculator,
60
60
  REPORT_FIELDS,
@@ -87,12 +87,12 @@ import {
87
87
  segmentByPeriod,
88
88
  synthesizeEdgeDecay,
89
89
  truncateTimeToMinute
90
- } from "./chunk-72GKJE2U.js";
91
- import "./chunk-CSDVJPBB.js";
92
- import "./chunk-WA5AAPCH.js";
93
- import "./chunk-5EBXHT6C.js";
94
- import "./chunk-NRFXAJF7.js";
95
- import "./chunk-FBNDMCT5.js";
90
+ } from "./chunk-BOPHW5M6.js";
91
+ import "./chunk-YUCOAJ4Z.js";
92
+ import "./chunk-GH2552SE.js";
93
+ import "./chunk-OBYKFW2B.js";
94
+ import "./chunk-W5E7FHC4.js";
95
+ import "./chunk-T66KH2XH.js";
96
96
  import "./chunk-UT3JLF3M.js";
97
97
 
98
98
  // src/index.ts
@@ -143,9 +143,7 @@ function formatRatio(value, decimals = 2) {
143
143
  // src/tools/shared/filters.ts
144
144
  function filterByStrategy(trades, strategy) {
145
145
  if (!strategy) return trades;
146
- return trades.filter(
147
- (t) => t.strategy.toLowerCase() === strategy.toLowerCase()
148
- );
146
+ return trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
149
147
  }
150
148
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
151
149
  function validateDateParam(date) {
@@ -338,13 +336,9 @@ function registerCoreBlockTools(server, baseDir) {
338
336
  inputSchema: z.object({
339
337
  sortBy: z.enum(["name", "tradeCount", "netPl", "dateRange"]).default("name").describe("Sort results by field (default: name)"),
340
338
  sortOrder: z.enum(["asc", "desc"]).default("asc").describe("Sort direction (default: asc)"),
341
- containsStrategy: z.string().optional().describe(
342
- "Filter to blocks containing this strategy name (case-insensitive)"
343
- ),
339
+ containsStrategy: z.string().optional().describe("Filter to blocks containing this strategy name (case-insensitive)"),
344
340
  minTrades: z.number().min(1).optional().describe("Filter to blocks with at least this many trades"),
345
- hasDailyLog: z.boolean().optional().describe(
346
- "Filter to blocks with (true) or without (false) daily log data"
347
- ),
341
+ hasDailyLog: z.boolean().optional().describe("Filter to blocks with (true) or without (false) daily log data"),
348
342
  hasReportingLog: z.boolean().optional().describe(
349
343
  "Filter to blocks with (true) or without (false) reporting log data (actual trade execution)"
350
344
  ),
@@ -353,15 +347,7 @@ function registerCoreBlockTools(server, baseDir) {
353
347
  },
354
348
  withFullSync(
355
349
  baseDir,
356
- async ({
357
- sortBy,
358
- sortOrder,
359
- containsStrategy,
360
- minTrades,
361
- hasDailyLog,
362
- hasReportingLog,
363
- limit
364
- }, { blockSyncResult: syncResult }) => {
350
+ async ({ sortBy, sortOrder, containsStrategy, minTrades, hasDailyLog, hasReportingLog, limit }, { blockSyncResult: syncResult }) => {
365
351
  try {
366
352
  let blocks = await listBlocks(baseDir);
367
353
  if (containsStrategy) {
@@ -400,9 +386,7 @@ function registerCoreBlockTools(server, baseDir) {
400
386
  if (limit !== void 0 && limit < blocks.length) {
401
387
  blocks = blocks.slice(0, limit);
402
388
  }
403
- const blocksWithReporting = blocks.filter(
404
- (b) => b.hasReportingLog
405
- ).length;
389
+ const blocksWithReporting = blocks.filter((b) => b.hasReportingLog).length;
406
390
  const summary = `Found ${blocks.length} block(s)${totalBeforeLimit > blocks.length ? ` (showing ${blocks.length} of ${totalBeforeLimit})` : ""}${blocksWithReporting > 0 ? `, ${blocksWithReporting} with reporting logs` : ""}`;
407
391
  const syncErrors = [...syncResult.errors];
408
392
  const structuredData = {
@@ -470,9 +454,7 @@ function registerCoreBlockTools(server, baseDir) {
470
454
  const block = await loadBlock(baseDir, blockId);
471
455
  const trades = block.trades;
472
456
  const dailyLogs = block.dailyLogs;
473
- const strategies = Array.from(
474
- new Set(trades.map((t) => t.strategy))
475
- ).sort();
457
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
476
458
  const dates = trades.map((t) => new Date(t.dateOpened).getTime());
477
459
  const dateRange = {
478
460
  start: dates.length > 0 ? new Date(Math.min(...dates)) : null,
@@ -549,10 +531,7 @@ function registerCoreBlockTools(server, baseDir) {
549
531
  const winRate = tradeCount > 0 ? winningTrades / tradeCount : 0;
550
532
  const totalPL2 = strategyTradeList.reduce((sum, t) => sum + t.pl, 0);
551
533
  const avgPL = tradeCount > 0 ? totalPL2 / tradeCount : 0;
552
- const contractCount = strategyTradeList.reduce(
553
- (sum, t) => sum + t.numContracts,
554
- 0
555
- );
534
+ const contractCount = strategyTradeList.reduce((sum, t) => sum + t.numContracts, 0);
556
535
  byStrategy[strategy] = { tradeCount, winRate, totalPL: totalPL2, avgPL, contractCount };
557
536
  }
558
537
  const totalPL = trades.reduce((sum, t) => sum + t.pl, 0);
@@ -596,9 +575,7 @@ function registerCoreBlockTools(server, baseDir) {
596
575
  inputSchema: z.object({
597
576
  blockId: z.string().describe("Block ID from list_blocks (e.g., 'main-port')"),
598
577
  strategy: z.string().optional().describe("Filter by strategy name (case-insensitive)"),
599
- tickerFilter: z.string().optional().describe(
600
- "Filter trades by underlying ticker symbol (e.g., 'SPY', 'AAPL')"
601
- ),
578
+ tickerFilter: z.string().optional().describe("Filter trades by underlying ticker symbol (e.g., 'SPY', 'AAPL')"),
602
579
  startDate: z.string().optional().describe("Start date filter (YYYY-MM-DD)"),
603
580
  endDate: z.string().optional().describe("End date filter (YYYY-MM-DD)")
604
581
  })
@@ -614,9 +591,7 @@ function registerCoreBlockTools(server, baseDir) {
614
591
  trades = filterByDateRange(trades, startDate, endDate);
615
592
  if (tickerFilter) {
616
593
  const tickerLower = tickerFilter.toLowerCase();
617
- trades = trades.filter(
618
- (t) => resolveTradeTicker(t).toLowerCase() === tickerLower
619
- );
594
+ trades = trades.filter((t) => resolveTradeTicker(t).toLowerCase() === tickerLower);
620
595
  }
621
596
  if (trades.length === 0) {
622
597
  return {
@@ -717,9 +692,7 @@ function registerComparisonBlockTools(server, baseDir) {
717
692
  endDate: z2.string().optional().describe("End date filter (YYYY-MM-DD)"),
718
693
  tickerFilter: z2.string().optional().describe("Filter trades by underlying ticker symbol"),
719
694
  minTrades: z2.number().min(1).optional().describe("Minimum trades per strategy to include in comparison"),
720
- sortBy: z2.enum(["netPl", "pl", "winRate", "trades", "profitFactor", "name"]).default("netPl").describe(
721
- "Sort strategies by metric (default: netPl). 'pl' is an alias for 'netPl'."
722
- ),
695
+ sortBy: z2.enum(["netPl", "pl", "winRate", "trades", "profitFactor", "name"]).default("netPl").describe("Sort strategies by metric (default: netPl). 'pl' is an alias for 'netPl'."),
723
696
  sortOrder: z2.enum(["asc", "desc"]).default("desc").describe("Sort direction (default: desc for highest first)"),
724
697
  limit: z2.number().min(1).optional().describe("Limit number of strategies shown")
725
698
  })
@@ -742,15 +715,11 @@ function registerComparisonBlockTools(server, baseDir) {
742
715
  trades = filterByDateRange(trades, startDate, endDate);
743
716
  if (tickerFilter) {
744
717
  const tickerLower = tickerFilter.toLowerCase();
745
- trades = trades.filter(
746
- (t) => resolveTradeTicker(t).toLowerCase() === tickerLower
747
- );
718
+ trades = trades.filter((t) => resolveTradeTicker(t).toLowerCase() === tickerLower);
748
719
  }
749
720
  if (trades.length === 0) {
750
721
  return {
751
- content: [
752
- { type: "text", text: "No trades found matching the filters." }
753
- ]
722
+ content: [{ type: "text", text: "No trades found matching the filters." }]
754
723
  };
755
724
  }
756
725
  const strategyStats = calculator.calculateStrategyStats(trades);
@@ -852,116 +821,104 @@ function registerComparisonBlockTools(server, baseDir) {
852
821
  sortOrder: z2.enum(["asc", "desc"]).default("asc").describe("Sort direction (default: asc)")
853
822
  })
854
823
  },
855
- withSyncedBlocks(
856
- baseDir,
857
- async ({ blockIds, metrics, sortBy, sortOrder }) => {
858
- try {
859
- const blockStats = [];
860
- for (const blockId of blockIds) {
861
- try {
862
- const block = await loadBlock(baseDir, blockId);
863
- const stats = calculator.calculatePortfolioStats(
864
- block.trades,
865
- block.dailyLogs
866
- );
867
- blockStats.push({ blockId, stats });
868
- } catch (error) {
869
- console.error(`Failed to load block ${blockId}:`, error);
870
- }
871
- }
872
- if (blockStats.length === 0) {
873
- return {
874
- content: [
875
- {
876
- type: "text",
877
- text: `Failed to load any of the specified blocks: ${blockIds.join(", ")}`
878
- }
879
- ],
880
- isError: true
881
- };
824
+ withSyncedBlocks(baseDir, async ({ blockIds, metrics, sortBy, sortOrder }) => {
825
+ try {
826
+ const blockStats = [];
827
+ for (const blockId of blockIds) {
828
+ try {
829
+ const block = await loadBlock(baseDir, blockId);
830
+ const stats = calculator.calculatePortfolioStats(block.trades, block.dailyLogs);
831
+ blockStats.push({ blockId, stats });
832
+ } catch (error) {
833
+ console.error(`Failed to load block ${blockId}:`, error);
882
834
  }
883
- const multiplier = sortOrder === "asc" ? 1 : -1;
884
- blockStats.sort((a, b) => {
885
- switch (sortBy) {
886
- case "totalTrades":
887
- return (a.stats.totalTrades - b.stats.totalTrades) * multiplier;
888
- case "winRate":
889
- return ((a.stats.winRate ?? 0) - (b.stats.winRate ?? 0)) * multiplier;
890
- case "netPl":
891
- return ((a.stats.netPl ?? 0) - (b.stats.netPl ?? 0)) * multiplier;
892
- case "sharpeRatio":
893
- return ((a.stats.sharpeRatio ?? 0) - (b.stats.sharpeRatio ?? 0)) * multiplier;
894
- case "sortinoRatio":
895
- return ((a.stats.sortinoRatio ?? 0) - (b.stats.sortinoRatio ?? 0)) * multiplier;
896
- case "maxDrawdown":
897
- return ((a.stats.maxDrawdown ?? 0) - (b.stats.maxDrawdown ?? 0)) * multiplier;
898
- case "profitFactor":
899
- return ((a.stats.profitFactor ?? 0) - (b.stats.profitFactor ?? 0)) * multiplier;
900
- case "calmarRatio":
901
- return ((a.stats.calmarRatio ?? 0) - (b.stats.calmarRatio ?? 0)) * multiplier;
902
- case "name":
903
- default:
904
- return a.blockId.localeCompare(b.blockId) * multiplier;
905
- }
906
- });
907
- const loadedIds = blockStats.map((b) => b.blockId);
908
- const failedIds = blockIds.filter((id) => !loadedIds.includes(id));
909
- const summary = `Block Comparison: ${blockStats.length} blocks loaded${failedIds.length > 0 ? ` (${failedIds.length} failed)` : ""} | Sorted by ${sortBy}`;
910
- const allMetrics = {
911
- totalTrades: true,
912
- winRate: true,
913
- netPl: true,
914
- sharpeRatio: true,
915
- sortinoRatio: true,
916
- maxDrawdown: true,
917
- profitFactor: true,
918
- calmarRatio: true
919
- };
920
- const requestedMetrics = metrics ? Object.fromEntries(metrics.map((m) => [m, true])) : allMetrics;
921
- const structuredData = {
922
- options: {
923
- metrics: metrics ?? null,
924
- sortBy,
925
- sortOrder
926
- },
927
- comparisons: blockStats.map(({ blockId, stats }) => {
928
- const filteredStats = {};
929
- if (requestedMetrics.totalTrades)
930
- filteredStats.totalTrades = stats.totalTrades;
931
- if (requestedMetrics.winRate)
932
- filteredStats.winRate = stats.winRate;
933
- if (requestedMetrics.netPl) filteredStats.netPl = stats.netPl;
934
- if (requestedMetrics.sharpeRatio)
935
- filteredStats.sharpeRatio = stats.sharpeRatio ?? null;
936
- if (requestedMetrics.sortinoRatio)
937
- filteredStats.sortinoRatio = stats.sortinoRatio ?? null;
938
- if (requestedMetrics.maxDrawdown)
939
- filteredStats.maxDrawdown = stats.maxDrawdown;
940
- if (requestedMetrics.profitFactor)
941
- filteredStats.profitFactor = stats.profitFactor;
942
- if (requestedMetrics.calmarRatio)
943
- filteredStats.calmarRatio = stats.calmarRatio ?? null;
944
- return {
945
- blockId,
946
- stats: filteredStats
947
- };
948
- }),
949
- failedBlocks: failedIds
950
- };
951
- return createToolOutput(summary, structuredData);
952
- } catch (error) {
835
+ }
836
+ if (blockStats.length === 0) {
953
837
  return {
954
838
  content: [
955
839
  {
956
840
  type: "text",
957
- text: `Error comparing blocks: ${error.message}`
841
+ text: `Failed to load any of the specified blocks: ${blockIds.join(", ")}`
958
842
  }
959
843
  ],
960
844
  isError: true
961
845
  };
962
846
  }
847
+ const multiplier = sortOrder === "asc" ? 1 : -1;
848
+ blockStats.sort((a, b) => {
849
+ switch (sortBy) {
850
+ case "totalTrades":
851
+ return (a.stats.totalTrades - b.stats.totalTrades) * multiplier;
852
+ case "winRate":
853
+ return ((a.stats.winRate ?? 0) - (b.stats.winRate ?? 0)) * multiplier;
854
+ case "netPl":
855
+ return ((a.stats.netPl ?? 0) - (b.stats.netPl ?? 0)) * multiplier;
856
+ case "sharpeRatio":
857
+ return ((a.stats.sharpeRatio ?? 0) - (b.stats.sharpeRatio ?? 0)) * multiplier;
858
+ case "sortinoRatio":
859
+ return ((a.stats.sortinoRatio ?? 0) - (b.stats.sortinoRatio ?? 0)) * multiplier;
860
+ case "maxDrawdown":
861
+ return ((a.stats.maxDrawdown ?? 0) - (b.stats.maxDrawdown ?? 0)) * multiplier;
862
+ case "profitFactor":
863
+ return ((a.stats.profitFactor ?? 0) - (b.stats.profitFactor ?? 0)) * multiplier;
864
+ case "calmarRatio":
865
+ return ((a.stats.calmarRatio ?? 0) - (b.stats.calmarRatio ?? 0)) * multiplier;
866
+ case "name":
867
+ default:
868
+ return a.blockId.localeCompare(b.blockId) * multiplier;
869
+ }
870
+ });
871
+ const loadedIds = blockStats.map((b) => b.blockId);
872
+ const failedIds = blockIds.filter((id) => !loadedIds.includes(id));
873
+ const summary = `Block Comparison: ${blockStats.length} blocks loaded${failedIds.length > 0 ? ` (${failedIds.length} failed)` : ""} | Sorted by ${sortBy}`;
874
+ const allMetrics = {
875
+ totalTrades: true,
876
+ winRate: true,
877
+ netPl: true,
878
+ sharpeRatio: true,
879
+ sortinoRatio: true,
880
+ maxDrawdown: true,
881
+ profitFactor: true,
882
+ calmarRatio: true
883
+ };
884
+ const requestedMetrics = metrics ? Object.fromEntries(metrics.map((m) => [m, true])) : allMetrics;
885
+ const structuredData = {
886
+ options: {
887
+ metrics: metrics ?? null,
888
+ sortBy,
889
+ sortOrder
890
+ },
891
+ comparisons: blockStats.map(({ blockId, stats }) => {
892
+ const filteredStats = {};
893
+ if (requestedMetrics.totalTrades) filteredStats.totalTrades = stats.totalTrades;
894
+ if (requestedMetrics.winRate) filteredStats.winRate = stats.winRate;
895
+ if (requestedMetrics.netPl) filteredStats.netPl = stats.netPl;
896
+ if (requestedMetrics.sharpeRatio) filteredStats.sharpeRatio = stats.sharpeRatio ?? null;
897
+ if (requestedMetrics.sortinoRatio)
898
+ filteredStats.sortinoRatio = stats.sortinoRatio ?? null;
899
+ if (requestedMetrics.maxDrawdown) filteredStats.maxDrawdown = stats.maxDrawdown;
900
+ if (requestedMetrics.profitFactor) filteredStats.profitFactor = stats.profitFactor;
901
+ if (requestedMetrics.calmarRatio) filteredStats.calmarRatio = stats.calmarRatio ?? null;
902
+ return {
903
+ blockId,
904
+ stats: filteredStats
905
+ };
906
+ }),
907
+ failedBlocks: failedIds
908
+ };
909
+ return createToolOutput(summary, structuredData);
910
+ } catch (error) {
911
+ return {
912
+ content: [
913
+ {
914
+ type: "text",
915
+ text: `Error comparing blocks: ${error.message}`
916
+ }
917
+ ],
918
+ isError: true
919
+ };
963
920
  }
964
- )
921
+ })
965
922
  );
966
923
  server.registerTool(
967
924
  "block_diff",
@@ -1017,10 +974,7 @@ function registerComparisonBlockTools(server, baseDir) {
1017
974
  shared.sort();
1018
975
  uniqueToA.sort();
1019
976
  uniqueToB.sort();
1020
- const totalUniqueStrategies = (/* @__PURE__ */ new Set([
1021
- ...strategiesA,
1022
- ...strategiesB
1023
- ])).size;
977
+ const totalUniqueStrategies = (/* @__PURE__ */ new Set([...strategiesA, ...strategiesB])).size;
1024
978
  const overlapPercent = totalUniqueStrategies > 0 ? shared.length / totalUniqueStrategies * 100 : 0;
1025
979
  const statsA = calculator.calculateStrategyStats(tradesA);
1026
980
  const statsB = calculator.calculateStrategyStats(tradesB);
@@ -1067,21 +1021,15 @@ function registerComparisonBlockTools(server, baseDir) {
1067
1021
  dailyLogsB && dailyLogsB.length > 0 ? dailyLogsB : void 0
1068
1022
  );
1069
1023
  const allMetrics = !metricsToCompare || metricsToCompare.length === 0;
1070
- const includeMetric = (m) => allMetrics || metricsToCompare?.includes(
1071
- m
1072
- );
1024
+ const includeMetric = (m) => allMetrics || metricsToCompare?.includes(m);
1073
1025
  const buildPortfolioEntry = (stats) => {
1074
1026
  const entry = {};
1075
1027
  if (includeMetric("trades")) entry.totalTrades = stats.totalTrades;
1076
- if (includeMetric("pl") || includeMetric("netPl"))
1077
- entry.netPl = stats.netPl;
1028
+ if (includeMetric("pl") || includeMetric("netPl")) entry.netPl = stats.netPl;
1078
1029
  if (includeMetric("winRate")) entry.winRate = stats.winRate;
1079
- if (includeMetric("profitFactor"))
1080
- entry.profitFactor = stats.profitFactor;
1081
- if (includeMetric("sharpeRatio"))
1082
- entry.sharpeRatio = stats.sharpeRatio ?? null;
1083
- if (includeMetric("maxDrawdown"))
1084
- entry.maxDrawdown = stats.maxDrawdown;
1030
+ if (includeMetric("profitFactor")) entry.profitFactor = stats.profitFactor;
1031
+ if (includeMetric("sharpeRatio")) entry.sharpeRatio = stats.sharpeRatio ?? null;
1032
+ if (includeMetric("maxDrawdown")) entry.maxDrawdown = stats.maxDrawdown;
1085
1033
  return entry;
1086
1034
  };
1087
1035
  const portfolioA = buildPortfolioEntry(portfolioStatsA);
@@ -1227,166 +1175,157 @@ function registerAnalysisBlockTools(server, baseDir) {
1227
1175
  )
1228
1176
  })
1229
1177
  },
1230
- withSyncedBlock(
1231
- baseDir,
1232
- async ({ blockId, scenarios, customScenarios, includeEmpty }) => {
1233
- try {
1234
- const block = await loadBlock(baseDir, blockId);
1235
- const trades = block.trades;
1236
- const sortedTrades = [...trades].sort(
1237
- (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
1238
- );
1239
- const portfolioStartDate = sortedTrades[0]?.dateOpened ? new Date(sortedTrades[0].dateOpened).toISOString().split("T")[0] : null;
1240
- const lastTrade = sortedTrades[sortedTrades.length - 1];
1241
- const portfolioEndDate = lastTrade?.dateClosed ? new Date(lastTrade.dateClosed).toISOString().split("T")[0] : null;
1242
- const scenariosToRun = [];
1243
- const preFilteredScenarioNames = [];
1244
- if (scenarios && scenarios.length > 0) {
1245
- const invalidScenarios = scenarios.filter(
1246
- (s) => !STRESS_SCENARIOS[s]
1247
- );
1248
- if (invalidScenarios.length > 0) {
1249
- return {
1250
- content: [
1251
- {
1252
- type: "text",
1253
- text: `Unknown scenario(s): ${invalidScenarios.join(", ")}. Available: ${Object.keys(STRESS_SCENARIOS).join(", ")}`
1254
- }
1255
- ],
1256
- isError: true
1257
- };
1258
- }
1259
- for (const scenarioName of scenarios) {
1260
- const scenario = STRESS_SCENARIOS[scenarioName];
1178
+ withSyncedBlock(baseDir, async ({ blockId, scenarios, customScenarios, includeEmpty }) => {
1179
+ try {
1180
+ const block = await loadBlock(baseDir, blockId);
1181
+ const trades = block.trades;
1182
+ const sortedTrades = [...trades].sort(
1183
+ (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
1184
+ );
1185
+ const portfolioStartDate = sortedTrades[0]?.dateOpened ? new Date(sortedTrades[0].dateOpened).toISOString().split("T")[0] : null;
1186
+ const lastTrade = sortedTrades[sortedTrades.length - 1];
1187
+ const portfolioEndDate = lastTrade?.dateClosed ? new Date(lastTrade.dateClosed).toISOString().split("T")[0] : null;
1188
+ const scenariosToRun = [];
1189
+ const preFilteredScenarioNames = [];
1190
+ if (scenarios && scenarios.length > 0) {
1191
+ const invalidScenarios = scenarios.filter((s) => !STRESS_SCENARIOS[s]);
1192
+ if (invalidScenarios.length > 0) {
1193
+ return {
1194
+ content: [
1195
+ {
1196
+ type: "text",
1197
+ text: `Unknown scenario(s): ${invalidScenarios.join(", ")}. Available: ${Object.keys(STRESS_SCENARIOS).join(", ")}`
1198
+ }
1199
+ ],
1200
+ isError: true
1201
+ };
1202
+ }
1203
+ for (const scenarioName of scenarios) {
1204
+ const scenario = STRESS_SCENARIOS[scenarioName];
1205
+ scenariosToRun.push({
1206
+ name: scenarioName,
1207
+ startDate: scenario.startDate,
1208
+ endDate: scenario.endDate,
1209
+ description: scenario.description,
1210
+ isCustom: false
1211
+ });
1212
+ }
1213
+ } else {
1214
+ for (const [name, scenario] of Object.entries(STRESS_SCENARIOS)) {
1215
+ const hasOverlap = portfolioStartDate !== null && portfolioEndDate !== null && scenario.endDate >= portfolioStartDate && scenario.startDate <= portfolioEndDate;
1216
+ if (hasOverlap) {
1261
1217
  scenariosToRun.push({
1262
- name: scenarioName,
1218
+ name,
1263
1219
  startDate: scenario.startDate,
1264
1220
  endDate: scenario.endDate,
1265
1221
  description: scenario.description,
1266
1222
  isCustom: false
1267
1223
  });
1268
- }
1269
- } else {
1270
- for (const [name, scenario] of Object.entries(STRESS_SCENARIOS)) {
1271
- const hasOverlap = portfolioStartDate !== null && portfolioEndDate !== null && scenario.endDate >= portfolioStartDate && scenario.startDate <= portfolioEndDate;
1272
- if (hasOverlap) {
1273
- scenariosToRun.push({
1274
- name,
1275
- startDate: scenario.startDate,
1276
- endDate: scenario.endDate,
1277
- description: scenario.description,
1278
- isCustom: false
1279
- });
1280
- } else {
1281
- preFilteredScenarioNames.push(name);
1282
- }
1224
+ } else {
1225
+ preFilteredScenarioNames.push(name);
1283
1226
  }
1284
1227
  }
1285
- if (customScenarios && customScenarios.length > 0) {
1286
- for (const custom of customScenarios) {
1287
- scenariosToRun.push({
1288
- name: custom.name,
1289
- startDate: custom.startDate,
1290
- endDate: custom.endDate,
1291
- description: `Custom scenario: ${custom.startDate} to ${custom.endDate}`,
1292
- isCustom: true
1293
- });
1294
- }
1228
+ }
1229
+ if (customScenarios && customScenarios.length > 0) {
1230
+ for (const custom of customScenarios) {
1231
+ scenariosToRun.push({
1232
+ name: custom.name,
1233
+ startDate: custom.startDate,
1234
+ endDate: custom.endDate,
1235
+ description: `Custom scenario: ${custom.startDate} to ${custom.endDate}`,
1236
+ isCustom: true
1237
+ });
1295
1238
  }
1296
- const scenarioResults = [];
1297
- let worstScenario = null;
1298
- let bestScenario = null;
1299
- let scenariosWithTrades = 0;
1300
- let scenariosSkipped = 0;
1301
- const skippedScenarioNames = [];
1302
- for (const scenario of scenariosToRun) {
1303
- const scenarioTrades = filterByDateRange(
1304
- trades,
1305
- scenario.startDate,
1306
- scenario.endDate
1307
- );
1308
- if (scenarioTrades.length === 0) {
1309
- scenariosSkipped++;
1310
- skippedScenarioNames.push(scenario.name);
1311
- if (includeEmpty) {
1312
- scenarioResults.push({
1313
- name: scenario.name,
1314
- description: scenario.description,
1315
- dateRange: { start: scenario.startDate, end: scenario.endDate },
1316
- tradeCount: 0,
1317
- stats: null,
1318
- isCustom: scenario.isCustom,
1319
- noCoverage: true
1320
- });
1321
- }
1322
- } else {
1323
- const stats = calculator.calculatePortfolioStats(
1324
- scenarioTrades,
1325
- void 0,
1326
- // No daily logs
1327
- true
1328
- // Force trade-based calculations
1329
- );
1239
+ }
1240
+ const scenarioResults = [];
1241
+ let worstScenario = null;
1242
+ let bestScenario = null;
1243
+ let scenariosWithTrades = 0;
1244
+ let scenariosSkipped = 0;
1245
+ const skippedScenarioNames = [];
1246
+ for (const scenario of scenariosToRun) {
1247
+ const scenarioTrades = filterByDateRange(trades, scenario.startDate, scenario.endDate);
1248
+ if (scenarioTrades.length === 0) {
1249
+ scenariosSkipped++;
1250
+ skippedScenarioNames.push(scenario.name);
1251
+ if (includeEmpty) {
1330
1252
  scenarioResults.push({
1331
1253
  name: scenario.name,
1332
1254
  description: scenario.description,
1333
1255
  dateRange: { start: scenario.startDate, end: scenario.endDate },
1334
- tradeCount: scenarioTrades.length,
1335
- stats: {
1336
- netPl: stats.netPl,
1337
- winRate: stats.winRate,
1338
- maxDrawdown: stats.maxDrawdown,
1339
- profitFactor: stats.profitFactor,
1340
- avgWin: stats.avgWin,
1341
- avgLoss: stats.avgLoss
1342
- },
1343
- isCustom: scenario.isCustom
1256
+ tradeCount: 0,
1257
+ stats: null,
1258
+ isCustom: scenario.isCustom,
1259
+ noCoverage: true
1344
1260
  });
1345
- scenariosWithTrades++;
1346
- if (worstScenario === null || stats.netPl < worstScenario.netPl) {
1347
- worstScenario = { name: scenario.name, netPl: stats.netPl };
1348
- }
1349
- if (bestScenario === null || stats.netPl > bestScenario.netPl) {
1350
- bestScenario = { name: scenario.name, netPl: stats.netPl };
1351
- }
1352
1261
  }
1353
- }
1354
- const summaryData = {
1355
- totalScenariosTested: scenariosToRun.length,
1356
- scenariosWithTrades,
1357
- scenariosSkipped,
1358
- ...skippedScenarioNames.length > 0 ? { skippedScenarios: skippedScenarioNames } : {},
1359
- ...preFilteredScenarioNames.length > 0 ? { preFilteredScenarios: preFilteredScenarioNames } : {},
1360
- worstScenario: worstScenario?.name ?? null,
1361
- bestScenario: bestScenario?.name ?? null,
1362
- portfolioDateRange: {
1363
- start: portfolioStartDate,
1364
- end: portfolioEndDate
1262
+ } else {
1263
+ const stats = calculator.calculatePortfolioStats(
1264
+ scenarioTrades,
1265
+ void 0,
1266
+ // No daily logs
1267
+ true
1268
+ // Force trade-based calculations
1269
+ );
1270
+ scenarioResults.push({
1271
+ name: scenario.name,
1272
+ description: scenario.description,
1273
+ dateRange: { start: scenario.startDate, end: scenario.endDate },
1274
+ tradeCount: scenarioTrades.length,
1275
+ stats: {
1276
+ netPl: stats.netPl,
1277
+ winRate: stats.winRate,
1278
+ maxDrawdown: stats.maxDrawdown,
1279
+ profitFactor: stats.profitFactor,
1280
+ avgWin: stats.avgWin,
1281
+ avgLoss: stats.avgLoss
1282
+ },
1283
+ isCustom: scenario.isCustom
1284
+ });
1285
+ scenariosWithTrades++;
1286
+ if (worstScenario === null || stats.netPl < worstScenario.netPl) {
1287
+ worstScenario = { name: scenario.name, netPl: stats.netPl };
1365
1288
  }
1366
- };
1367
- const skippedNote = scenariosSkipped > 0 ? ` (${scenariosSkipped} skipped - no data coverage)` : "";
1368
- const preFilterNote = preFilteredScenarioNames.length > 0 ? ` (${preFilteredScenarioNames.length} excluded - outside portfolio date range)` : "";
1369
- const summary = `Stress Test: ${blockId} | ${scenariosWithTrades} scenarios with trades${skippedNote}${preFilterNote} | Worst: ${worstScenario?.name ?? "N/A"} (${worstScenario ? formatCurrency(worstScenario.netPl) : "N/A"}) | Best: ${bestScenario?.name ?? "N/A"} (${bestScenario ? formatCurrency(bestScenario.netPl) : "N/A"})`;
1370
- const structuredData = {
1371
- blockId,
1372
- scenarios: scenarioResults,
1373
- summary: summaryData,
1374
- availableBuiltInScenarios: Object.keys(STRESS_SCENARIOS)
1375
- };
1376
- return createToolOutput(summary, structuredData);
1377
- } catch (error) {
1378
- return {
1379
- content: [
1380
- {
1381
- type: "text",
1382
- text: `Error running stress test: ${error.message}`
1383
- }
1384
- ],
1385
- isError: true
1386
- };
1289
+ if (bestScenario === null || stats.netPl > bestScenario.netPl) {
1290
+ bestScenario = { name: scenario.name, netPl: stats.netPl };
1291
+ }
1292
+ }
1387
1293
  }
1294
+ const summaryData = {
1295
+ totalScenariosTested: scenariosToRun.length,
1296
+ scenariosWithTrades,
1297
+ scenariosSkipped,
1298
+ ...skippedScenarioNames.length > 0 ? { skippedScenarios: skippedScenarioNames } : {},
1299
+ ...preFilteredScenarioNames.length > 0 ? { preFilteredScenarios: preFilteredScenarioNames } : {},
1300
+ worstScenario: worstScenario?.name ?? null,
1301
+ bestScenario: bestScenario?.name ?? null,
1302
+ portfolioDateRange: {
1303
+ start: portfolioStartDate,
1304
+ end: portfolioEndDate
1305
+ }
1306
+ };
1307
+ const skippedNote = scenariosSkipped > 0 ? ` (${scenariosSkipped} skipped - no data coverage)` : "";
1308
+ const preFilterNote = preFilteredScenarioNames.length > 0 ? ` (${preFilteredScenarioNames.length} excluded - outside portfolio date range)` : "";
1309
+ const summary = `Stress Test: ${blockId} | ${scenariosWithTrades} scenarios with trades${skippedNote}${preFilterNote} | Worst: ${worstScenario?.name ?? "N/A"} (${worstScenario ? formatCurrency(worstScenario.netPl) : "N/A"}) | Best: ${bestScenario?.name ?? "N/A"} (${bestScenario ? formatCurrency(bestScenario.netPl) : "N/A"})`;
1310
+ const structuredData = {
1311
+ blockId,
1312
+ scenarios: scenarioResults,
1313
+ summary: summaryData,
1314
+ availableBuiltInScenarios: Object.keys(STRESS_SCENARIOS)
1315
+ };
1316
+ return createToolOutput(summary, structuredData);
1317
+ } catch (error) {
1318
+ return {
1319
+ content: [
1320
+ {
1321
+ type: "text",
1322
+ text: `Error running stress test: ${error.message}`
1323
+ }
1324
+ ],
1325
+ isError: true
1326
+ };
1388
1327
  }
1389
- )
1328
+ })
1390
1329
  );
1391
1330
  server.registerTool(
1392
1331
  "drawdown_attribution",
@@ -1394,9 +1333,7 @@ function registerAnalysisBlockTools(server, baseDir) {
1394
1333
  description: "Identify which strategies contributed most to losses during the portfolio's maximum drawdown period. Shows drawdown period (peak to trough) and per-strategy P/L attribution.",
1395
1334
  inputSchema: z3.object({
1396
1335
  blockId: z3.string().describe("Block folder name"),
1397
- strategy: z3.string().optional().describe(
1398
- "Optional: Filter to specific strategy before calculating drawdown"
1399
- ),
1336
+ strategy: z3.string().optional().describe("Optional: Filter to specific strategy before calculating drawdown"),
1400
1337
  topN: z3.number().int().min(1).max(50).default(5).describe("Number of top contributors to return (default: 5)")
1401
1338
  })
1402
1339
  },
@@ -1429,9 +1366,7 @@ function registerAnalysisBlockTools(server, baseDir) {
1429
1366
  const initialCapital = (firstTrade.fundsAtClose ?? 1e4) - firstTrade.pl;
1430
1367
  let equity = initialCapital;
1431
1368
  let peakEquity = initialCapital;
1432
- let peakDate = new Date(
1433
- firstTrade.dateClosed ?? firstTrade.dateOpened
1434
- );
1369
+ let peakDate = new Date(firstTrade.dateClosed ?? firstTrade.dateOpened);
1435
1370
  let maxDrawdown = 0;
1436
1371
  let maxDrawdownPct = 0;
1437
1372
  let troughDate = null;
@@ -1565,9 +1500,7 @@ function registerAnalysisBlockTools(server, baseDir) {
1565
1500
  ]
1566
1501
  };
1567
1502
  }
1568
- const strategies = Array.from(
1569
- new Set(trades.map((t) => t.strategy))
1570
- ).sort();
1503
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
1571
1504
  if (targetStrategy) {
1572
1505
  const matchedStrategy = strategies.find(
1573
1506
  (s) => s.toLowerCase() === targetStrategy.toLowerCase()
@@ -1586,10 +1519,7 @@ function registerAnalysisBlockTools(server, baseDir) {
1586
1519
  }
1587
1520
  if (strategies.length === 1) {
1588
1521
  const dailyLogs2 = block.dailyLogs && block.dailyLogs.length > 0 ? block.dailyLogs : void 0;
1589
- const baselineStats2 = calculator.calculatePortfolioStats(
1590
- trades,
1591
- dailyLogs2
1592
- );
1522
+ const baselineStats2 = calculator.calculatePortfolioStats(trades, dailyLogs2);
1593
1523
  const summary2 = `Marginal Contribution: ${blockId} | Single strategy portfolio - cannot calculate marginal contribution`;
1594
1524
  const structuredData2 = {
1595
1525
  blockId,
@@ -1617,13 +1547,8 @@ function registerAnalysisBlockTools(server, baseDir) {
1617
1547
  return createToolOutput(summary2, structuredData2);
1618
1548
  }
1619
1549
  const dailyLogs = block.dailyLogs && block.dailyLogs.length > 0 ? block.dailyLogs : void 0;
1620
- const baselineStats = calculator.calculatePortfolioStats(
1621
- trades,
1622
- dailyLogs
1623
- );
1624
- const strategiesToAnalyze = targetStrategy ? strategies.filter(
1625
- (s) => s.toLowerCase() === targetStrategy.toLowerCase()
1626
- ) : strategies;
1550
+ const baselineStats = calculator.calculatePortfolioStats(trades, dailyLogs);
1551
+ const strategiesToAnalyze = targetStrategy ? strategies.filter((s) => s.toLowerCase() === targetStrategy.toLowerCase()) : strategies;
1627
1552
  const contributions = [];
1628
1553
  for (const strategy of strategiesToAnalyze) {
1629
1554
  const tradesWithout = trades.filter(
@@ -1666,21 +1591,20 @@ function registerAnalysisBlockTools(server, baseDir) {
1666
1591
  });
1667
1592
  const limitedContributions = targetStrategy ? contributions : contributions.slice(0, topN);
1668
1593
  const validContributions = contributions.filter((c) => c.marginalSharpe !== null);
1669
- const mostBeneficial = validContributions.length > 0 ? { strategy: validContributions[0].strategy, sharpe: validContributions[0].marginalSharpe } : null;
1594
+ const mostBeneficial = validContributions.length > 0 ? {
1595
+ strategy: validContributions[0].strategy,
1596
+ sharpe: validContributions[0].marginalSharpe
1597
+ } : null;
1670
1598
  const lastValid = validContributions[validContributions.length - 1];
1671
1599
  const leastBeneficial = validContributions.length > 0 ? { strategy: lastValid.strategy, sharpe: lastValid.marginalSharpe } : null;
1672
1600
  const summaryParts = [`Marginal Contribution: ${blockId}`];
1673
1601
  if (mostBeneficial && mostBeneficial.sharpe !== null) {
1674
1602
  const sharpeStr = mostBeneficial.sharpe >= 0 ? `+${formatRatio(mostBeneficial.sharpe)}` : formatRatio(mostBeneficial.sharpe);
1675
- summaryParts.push(
1676
- `Top: ${mostBeneficial.strategy} (Sharpe ${sharpeStr})`
1677
- );
1603
+ summaryParts.push(`Top: ${mostBeneficial.strategy} (Sharpe ${sharpeStr})`);
1678
1604
  }
1679
1605
  if (leastBeneficial && leastBeneficial.sharpe !== null && leastBeneficial.strategy !== mostBeneficial?.strategy) {
1680
1606
  const sharpeStr = leastBeneficial.sharpe >= 0 ? `+${formatRatio(leastBeneficial.sharpe)}` : formatRatio(leastBeneficial.sharpe);
1681
- summaryParts.push(
1682
- `Worst: ${leastBeneficial.strategy} (Sharpe ${sharpeStr})`
1683
- );
1607
+ summaryParts.push(`Worst: ${leastBeneficial.strategy} (Sharpe ${sharpeStr})`);
1684
1608
  }
1685
1609
  const summary = summaryParts.join(" | ");
1686
1610
  const structuredData = {
@@ -1774,9 +1698,7 @@ function registerSimilarityBlockTools(server, baseDir) {
1774
1698
  ]
1775
1699
  };
1776
1700
  }
1777
- const strategies = Array.from(
1778
- new Set(trades.map((t) => t.strategy))
1779
- ).sort();
1701
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
1780
1702
  if (strategies.length < 2) {
1781
1703
  return {
1782
1704
  content: [
@@ -1868,8 +1790,7 @@ function registerSimilarityBlockTools(server, baseDir) {
1868
1790
  }
1869
1791
  }
1870
1792
  pairs.sort((a, b) => {
1871
- if (a.compositeSimilarity === null && b.compositeSimilarity === null)
1872
- return 0;
1793
+ if (a.compositeSimilarity === null && b.compositeSimilarity === null) return 0;
1873
1794
  if (a.compositeSimilarity === null) return 1;
1874
1795
  if (b.compositeSimilarity === null) return -1;
1875
1796
  return b.compositeSimilarity - a.compositeSimilarity;
@@ -1915,23 +1836,40 @@ function registerSimilarityBlockTools(server, baseDir) {
1915
1836
  {
1916
1837
  description: "Explore strategy weight combinations within a portfolio. Answer 'what if I scaled strategy X to 0.5x?' questions. Shows before/after comparison with per-strategy breakdown. Profile-aware: uses backtest block data, enforces maxContractsPerTrade ceilings, flags ignoreMarginReq. Multi-strategy mode combines trades from multiple blocks.",
1917
1838
  inputSchema: z4.object({
1918
- blockId: z4.string().describe("Block folder name (required for single-strategy mode, optional default for multi-strategy mode)"),
1839
+ blockId: z4.string().describe(
1840
+ "Block folder name (required for single-strategy mode, optional default for multi-strategy mode)"
1841
+ ),
1919
1842
  strategyWeights: z4.record(z4.string(), z4.number().min(0).max(2)).optional().describe(
1920
1843
  'Weight per strategy, e.g., {"5/7 17\u0394": 0.5}. Unspecified strategies default to 1.0. Weight 0 = exclude strategy entirely. Max weight: 2.0. Ignored when strategies array is provided.'
1921
1844
  ),
1922
- strategies: z4.array(z4.object({
1923
- strategyName: z4.string().describe("Strategy name matching a stored profile"),
1924
- blockId: z4.string().describe("Block ID to source trades from (overrides top-level blockId for this strategy)"),
1925
- scaleFactor: z4.number().min(0).max(5).describe("Scale factor for this strategy (1.0 = current allocation)")
1926
- })).optional().describe("Multi-strategy mode: array of strategies with per-strategy block source and scale. When provided, ignores strategyWeights."),
1927
- showUncapped: z4.boolean().optional().default(false).describe("When true, also run without maxContractsPerTrade ceiling for side-by-side comparison"),
1845
+ strategies: z4.array(
1846
+ z4.object({
1847
+ strategyName: z4.string().describe("Strategy name matching a stored profile"),
1848
+ blockId: z4.string().describe(
1849
+ "Block ID to source trades from (overrides top-level blockId for this strategy)"
1850
+ ),
1851
+ scaleFactor: z4.number().min(0).max(5).describe("Scale factor for this strategy (1.0 = current allocation)")
1852
+ })
1853
+ ).optional().describe(
1854
+ "Multi-strategy mode: array of strategies with per-strategy block source and scale. When provided, ignores strategyWeights."
1855
+ ),
1856
+ showUncapped: z4.boolean().optional().default(false).describe(
1857
+ "When true, also run without maxContractsPerTrade ceiling for side-by-side comparison"
1858
+ ),
1928
1859
  startDate: z4.string().optional().describe("Start date filter (YYYY-MM-DD)"),
1929
1860
  endDate: z4.string().optional().describe("End date filter (YYYY-MM-DD)")
1930
1861
  })
1931
1862
  },
1932
1863
  withSyncedBlock(
1933
1864
  baseDir,
1934
- async ({ blockId, strategyWeights, strategies: strategiesInput, showUncapped, startDate, endDate }) => {
1865
+ async ({
1866
+ blockId,
1867
+ strategyWeights,
1868
+ strategies: strategiesInput,
1869
+ showUncapped,
1870
+ startDate,
1871
+ endDate
1872
+ }) => {
1935
1873
  try {
1936
1874
  let filterTradesByStrategy2 = function(allTrades, strategyName) {
1937
1875
  const matched = allTrades.filter(
@@ -2024,7 +1962,10 @@ function registerSimilarityBlockTools(server, baseDir) {
2024
1962
  const dataAvailability = [];
2025
1963
  for (const entry of strategiesInput) {
2026
1964
  const sourceBlockId = entry.blockId || blockId;
2027
- const { profile, status: profileStatus } = await tryGetProfile(sourceBlockId, entry.strategyName);
1965
+ const { profile, status: profileStatus } = await tryGetProfile(
1966
+ sourceBlockId,
1967
+ entry.strategyName
1968
+ );
2028
1969
  let tradeSourceBlockId = sourceBlockId;
2029
1970
  let dataSource = "multi_strategy_input";
2030
1971
  if (profile && profile.blockId !== sourceBlockId) {
@@ -2153,7 +2094,11 @@ function registerSimilarityBlockTools(server, baseDir) {
2153
2094
  }
2154
2095
  if (uncappedScaled.length > 0) {
2155
2096
  const uncappedModified = buildModifiedTrades2(uncappedScaled, allOriginalTrades);
2156
- const uncappedStats = calculator.calculatePortfolioStats(uncappedModified, void 0, true);
2097
+ const uncappedStats = calculator.calculatePortfolioStats(
2098
+ uncappedModified,
2099
+ void 0,
2100
+ true
2101
+ );
2157
2102
  uncappedComparison2 = {
2158
2103
  sharpeRatio: uncappedStats.sharpeRatio,
2159
2104
  sortinoRatio: uncappedStats.sortinoRatio,
@@ -2197,9 +2142,7 @@ function registerSimilarityBlockTools(server, baseDir) {
2197
2142
  isError: true
2198
2143
  };
2199
2144
  }
2200
- const allStrategies = Array.from(
2201
- new Set(trades.map((t) => t.strategy))
2202
- ).sort();
2145
+ const allStrategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
2203
2146
  const appliedWeights = {};
2204
2147
  const unknownStrategies = [];
2205
2148
  for (const strategy of allStrategies) {
@@ -2217,9 +2160,7 @@ function registerSimilarityBlockTools(server, baseDir) {
2217
2160
  }
2218
2161
  }
2219
2162
  }
2220
- const allZeroWeight = Object.values(appliedWeights).every(
2221
- (w) => w === 0
2222
- );
2163
+ const allZeroWeight = Object.values(appliedWeights).every((w) => w === 0);
2223
2164
  if (allZeroWeight) {
2224
2165
  return {
2225
2166
  content: [
@@ -2278,22 +2219,31 @@ function registerSimilarityBlockTools(server, baseDir) {
2278
2219
  hasCeilings ? maxContractsCeilings : void 0
2279
2220
  );
2280
2221
  const modifiedTrades = buildModifiedTrades2(scaledTrades, trades);
2281
- const scaledStats = calculator.calculatePortfolioStats(
2282
- modifiedTrades,
2283
- void 0,
2284
- true
2285
- );
2222
+ const scaledStats = calculator.calculatePortfolioStats(modifiedTrades, void 0, true);
2286
2223
  let uncappedComparison;
2287
2224
  if (doShowUncapped && cappedStrategies.size > 0) {
2288
2225
  const { scaledTrades: uncappedScaled } = buildScaledTrades2(tradesToUse, appliedWeights);
2289
2226
  const uncappedModified = buildModifiedTrades2(uncappedScaled, trades);
2290
- const uncappedStats = calculator.calculatePortfolioStats(uncappedModified, void 0, true);
2227
+ const uncappedStats = calculator.calculatePortfolioStats(
2228
+ uncappedModified,
2229
+ void 0,
2230
+ true
2231
+ );
2291
2232
  uncappedComparison = {
2292
- sharpeRatio: calcDelta2(baselineStats.sharpeRatio ?? null, uncappedStats.sharpeRatio ?? null),
2293
- sortinoRatio: calcDelta2(baselineStats.sortinoRatio ?? null, uncappedStats.sortinoRatio ?? null),
2233
+ sharpeRatio: calcDelta2(
2234
+ baselineStats.sharpeRatio ?? null,
2235
+ uncappedStats.sharpeRatio ?? null
2236
+ ),
2237
+ sortinoRatio: calcDelta2(
2238
+ baselineStats.sortinoRatio ?? null,
2239
+ uncappedStats.sortinoRatio ?? null
2240
+ ),
2294
2241
  maxDrawdown: calcDelta2(baselineStats.maxDrawdown, uncappedStats.maxDrawdown),
2295
2242
  netPl: calcDelta2(baselineStats.netPl, uncappedStats.netPl),
2296
- totalTrades: { original: baselineStats.totalTrades, scaled: uncappedStats.totalTrades }
2243
+ totalTrades: {
2244
+ original: baselineStats.totalTrades,
2245
+ scaled: uncappedStats.totalTrades
2246
+ }
2297
2247
  };
2298
2248
  }
2299
2249
  const comparison = {
@@ -2305,10 +2255,7 @@ function registerSimilarityBlockTools(server, baseDir) {
2305
2255
  baselineStats.sortinoRatio ?? null,
2306
2256
  scaledStats.sortinoRatio ?? null
2307
2257
  ),
2308
- maxDrawdown: calcDelta2(
2309
- baselineStats.maxDrawdown,
2310
- scaledStats.maxDrawdown
2311
- ),
2258
+ maxDrawdown: calcDelta2(baselineStats.maxDrawdown, scaledStats.maxDrawdown),
2312
2259
  netPl: calcDelta2(baselineStats.netPl, scaledStats.netPl),
2313
2260
  totalTrades: {
2314
2261
  original: baselineStats.totalTrades,
@@ -2338,10 +2285,9 @@ function registerSimilarityBlockTools(server, baseDir) {
2338
2285
  scaledByStrategy[st.strategy].netPl += netPl;
2339
2286
  totalScaledPl += netPl;
2340
2287
  }
2341
- const allStrategyNames = Array.from(/* @__PURE__ */ new Set([
2342
- ...allStrategies,
2343
- ...Object.keys(originalByStrategy)
2344
- ])).sort();
2288
+ const allStrategyNames = Array.from(
2289
+ /* @__PURE__ */ new Set([...allStrategies, ...Object.keys(originalByStrategy)])
2290
+ ).sort();
2345
2291
  for (const strategy of allStrategyNames) {
2346
2292
  const weight = appliedWeights[strategy] ?? 1;
2347
2293
  const orig = originalByStrategy[strategy] ?? { trades: 0, netPl: 0 };
@@ -2400,12 +2346,14 @@ function registerSimilarityBlockTools(server, baseDir) {
2400
2346
  };
2401
2347
  const anyProfileFound = Object.values(profileLookups).some((l) => l.status === "found");
2402
2348
  if (anyProfileFound) {
2403
- structuredData.dataAvailability = Object.entries(profileLookups).map(([strategy, lookup]) => ({
2404
- strategy,
2405
- profileStatus: lookup.status,
2406
- profileBlockId: lookup.profile?.blockId,
2407
- dataSource: dataSourceMap[strategy]
2408
- }));
2349
+ structuredData.dataAvailability = Object.entries(profileLookups).map(
2350
+ ([strategy, lookup]) => ({
2351
+ strategy,
2352
+ profileStatus: lookup.status,
2353
+ profileBlockId: lookup.profile?.blockId,
2354
+ dataSource: dataSourceMap[strategy]
2355
+ })
2356
+ );
2409
2357
  }
2410
2358
  if (backtestSubstitutions.length > 0) {
2411
2359
  structuredData.backtestSubstitutions = backtestSubstitutions;
@@ -3363,10 +3311,22 @@ var VIX_OHLCV_MAPPINGS = [
3363
3311
  { alias: "VIX_Low", tableAlias: "vix", sourceCol: "low", ticker: "VIX", timing: "close" },
3364
3312
  // VIX9D
3365
3313
  { alias: "VIX9D_Open", tableAlias: "vix9d", sourceCol: "open", ticker: "VIX9D", timing: "open" },
3366
- { alias: "VIX9D_Close", tableAlias: "vix9d", sourceCol: "close", ticker: "VIX9D", timing: "close" },
3314
+ {
3315
+ alias: "VIX9D_Close",
3316
+ tableAlias: "vix9d",
3317
+ sourceCol: "close",
3318
+ ticker: "VIX9D",
3319
+ timing: "close"
3320
+ },
3367
3321
  // VIX3M
3368
3322
  { alias: "VIX3M_Open", tableAlias: "vix3m", sourceCol: "open", ticker: "VIX3M", timing: "open" },
3369
- { alias: "VIX3M_Close", tableAlias: "vix3m", sourceCol: "close", ticker: "VIX3M", timing: "close" }
3323
+ {
3324
+ alias: "VIX3M_Close",
3325
+ tableAlias: "vix3m",
3326
+ sourceCol: "close",
3327
+ ticker: "VIX3M",
3328
+ timing: "close"
3329
+ }
3370
3330
  ];
3371
3331
  var VIX_ENRICHED_MAPPINGS = [
3372
3332
  { alias: "VIX_IVR", tableAlias: "evix", sourceCol: "ivr", ticker: "VIX", timing: "close" },
@@ -3376,10 +3336,7 @@ var VIX_ENRICHED_MAPPINGS = [
3376
3336
  { alias: "VIX3M_IVR", tableAlias: "evix3m", sourceCol: "ivr", ticker: "VIX3M", timing: "close" },
3377
3337
  { alias: "VIX3M_IVP", tableAlias: "evix3m", sourceCol: "ivp", ticker: "VIX3M", timing: "close" }
3378
3338
  ];
3379
- var VIX_ALL_MAPPINGS = [
3380
- ...VIX_OHLCV_MAPPINGS,
3381
- ...VIX_ENRICHED_MAPPINGS
3382
- ];
3339
+ var VIX_ALL_MAPPINGS = [...VIX_OHLCV_MAPPINGS, ...VIX_ENRICHED_MAPPINGS];
3383
3340
  var VIX_TICKER_ALIASES = [...new Set(VIX_OHLCV_MAPPINGS.map((m) => m.tableAlias))];
3384
3341
  var VIX_TICKER_FOR_ALIAS = Object.fromEntries([
3385
3342
  ...VIX_OHLCV_MAPPINGS.map((m) => [m.tableAlias, m.ticker]),
@@ -3416,9 +3373,7 @@ var CLOSE_KNOWN_FIELDS = /* @__PURE__ */ new Set([
3416
3373
  ...DAILY_CLOSE_FIELDS,
3417
3374
  ...CONTEXT_CLOSE_FIELDS
3418
3375
  ]);
3419
- var STATIC_FIELDS = /* @__PURE__ */ new Set([
3420
- ...DAILY_STATIC_FIELDS
3421
- ]);
3376
+ var STATIC_FIELDS = /* @__PURE__ */ new Set([...DAILY_STATIC_FIELDS]);
3422
3377
  function buildVixJoinClause(tickerAliases, baseAlias = "d") {
3423
3378
  return tickerAliases.flatMap((alias) => {
3424
3379
  const ticker = VIX_TICKER_FOR_ALIAS[alias];
@@ -3543,7 +3498,11 @@ function buildOutcomeQuery(tradeDatesOrKeys) {
3543
3498
  if (typeof tradeDatesOrKeys[0] === "string") {
3544
3499
  return buildOutcomeQueryForDates(tradeDatesOrKeys, vixCloseCols, derivedCloseCols);
3545
3500
  }
3546
- return buildOutcomeQueryForKeys(tradeDatesOrKeys, vixCloseCols, derivedCloseCols);
3501
+ return buildOutcomeQueryForKeys(
3502
+ tradeDatesOrKeys,
3503
+ vixCloseCols,
3504
+ derivedCloseCols
3505
+ );
3547
3506
  }
3548
3507
  function buildTargetCloseCols(eAlias, sAlias) {
3549
3508
  return [...DAILY_CLOSE_FIELDS].map((f) => `${OHLCV_COLS.has(f) ? sAlias : eAlias}."${f}"`).join(", ");
@@ -3892,8 +3851,14 @@ function buildConcentrationSection(profiles) {
3892
3851
  else if (concentratedDimensions === 2) grade = "C";
3893
3852
  else grade = "F";
3894
3853
  }
3895
- for (const [dim, groups] of Object.entries({ structure: byStructure, underlying: byUnderlying, dte: byDte })) {
3896
- const topCategory = Object.entries(groups).sort((a, b) => b[1].allocationPct - a[1].allocationPct)[0];
3854
+ for (const [dim, groups] of Object.entries({
3855
+ structure: byStructure,
3856
+ underlying: byUnderlying,
3857
+ dte: byDte
3858
+ })) {
3859
+ const topCategory = Object.entries(groups).sort(
3860
+ (a, b) => b[1].allocationPct - a[1].allocationPct
3861
+ )[0];
3897
3862
  if (topCategory) {
3898
3863
  flags.push({
3899
3864
  type: "info",
@@ -3923,11 +3888,13 @@ function buildCorrelationRiskSection(profiles) {
3923
3888
  if (profiles.length < 2) {
3924
3889
  return {
3925
3890
  grade: null,
3926
- flags: [{
3927
- type: "info",
3928
- dimension: "correlationRisk",
3929
- message: "Skipped: need at least 2 profiles for correlation risk analysis"
3930
- }],
3891
+ flags: [
3892
+ {
3893
+ type: "info",
3894
+ dimension: "correlationRisk",
3895
+ message: "Skipped: need at least 2 profiles for correlation risk analysis"
3896
+ }
3897
+ ],
3931
3898
  data: {},
3932
3899
  keyNumbers: {}
3933
3900
  };
@@ -4161,9 +4128,7 @@ function registerHealthBlockTools(server, baseDir) {
4161
4128
  isError: true
4162
4129
  };
4163
4130
  }
4164
- const strategies = Array.from(
4165
- new Set(trades.map((t) => t.strategy))
4166
- ).sort();
4131
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
4167
4132
  if (strategies.length < 2) {
4168
4133
  return {
4169
4134
  content: [
@@ -4317,9 +4282,7 @@ function registerHealthBlockTools(server, baseDir) {
4317
4282
  });
4318
4283
  }
4319
4284
  const corrStrategyIndex = /* @__PURE__ */ new Map();
4320
- correlationMatrix.strategies.forEach(
4321
- (s, i) => corrStrategyIndex.set(s, i)
4322
- );
4285
+ correlationMatrix.strategies.forEach((s, i) => corrStrategyIndex.set(s, i));
4323
4286
  const highTailPairs = [];
4324
4287
  for (let i = 0; i < tailRisk.strategies.length; i++) {
4325
4288
  for (let j = i + 1; j < tailRisk.strategies.length; j++) {
@@ -4546,15 +4509,11 @@ function registerHealthBlockTools(server, baseDir) {
4546
4509
  oneLineSummary = "Portfolio shows strong diversification, controlled tail risk, and consistent Monte Carlo outcomes.";
4547
4510
  } else if (flagCount <= 2) {
4548
4511
  verdict = "MODERATE_CONCERNS";
4549
- const concernDimensions = [
4550
- ...new Set(warningFlags.map((f) => f.dimension))
4551
- ];
4512
+ const concernDimensions = [...new Set(warningFlags.map((f) => f.dimension))];
4552
4513
  oneLineSummary = `Portfolio has ${flagCount} warning(s) in ${concernDimensions.join(", ")} - review flagged items.`;
4553
4514
  } else {
4554
4515
  verdict = "ISSUES_DETECTED";
4555
- const concernDimensions = [
4556
- ...new Set(warningFlags.map((f) => f.dimension))
4557
- ];
4516
+ const concernDimensions = [...new Set(warningFlags.map((f) => f.dimension))];
4558
4517
  oneLineSummary = `Portfolio has ${flagCount} warnings across ${concernDimensions.join(", ")} - significant review recommended.`;
4559
4518
  }
4560
4519
  const keyNumbers = {
@@ -4643,9 +4602,7 @@ function registerBlockTools(server, baseDir) {
4643
4602
  import { z as z6 } from "zod";
4644
4603
  function filterByStrategy2(trades, strategy) {
4645
4604
  if (!strategy) return trades;
4646
- return trades.filter(
4647
- (t) => t.strategy.toLowerCase() === strategy.toLowerCase()
4648
- );
4605
+ return trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
4649
4606
  }
4650
4607
  function registerAnalysisTools(server, baseDir) {
4651
4608
  server.registerTool(
@@ -4669,9 +4626,7 @@ function registerAnalysisTools(server, baseDir) {
4669
4626
  outOfSampleDays: z6.number().min(1).optional().describe(
4670
4627
  "Explicit out-of-sample period in days. Overrides oosWindowCount calculation if provided."
4671
4628
  ),
4672
- stepSizeDays: z6.number().min(1).optional().describe(
4673
- "Days to slide forward each period. If not provided, equals outOfSampleDays."
4674
- ),
4629
+ stepSizeDays: z6.number().min(1).optional().describe("Days to slide forward each period. If not provided, equals outOfSampleDays."),
4675
4630
  // Optimization settings
4676
4631
  optimizationTarget: z6.enum([
4677
4632
  "netPl",
@@ -4691,26 +4646,43 @@ function registerAnalysisTools(server, baseDir) {
4691
4646
  selectedStrategies: z6.array(z6.string()).optional().describe("Filter to specific strategies only (default: all strategies)"),
4692
4647
  // Additional filters
4693
4648
  tickerFilter: z6.string().optional().describe("Filter trades by underlying ticker symbol (e.g., 'SPY', 'AAPL')"),
4694
- dateRangeFrom: z6.string().optional().describe("Start date for analysis (ISO format: YYYY-MM-DD). Only include trades on or after this date."),
4695
- dateRangeTo: z6.string().optional().describe("End date for analysis (ISO format: YYYY-MM-DD). Only include trades on or before this date."),
4649
+ dateRangeFrom: z6.string().optional().describe(
4650
+ "Start date for analysis (ISO format: YYYY-MM-DD). Only include trades on or after this date."
4651
+ ),
4652
+ dateRangeTo: z6.string().optional().describe(
4653
+ "End date for analysis (ISO format: YYYY-MM-DD). Only include trades on or before this date."
4654
+ ),
4696
4655
  // Performance floor constraints (reject parameter combinations that don't meet minimums)
4697
- minSharpeRatio: z6.number().optional().describe("Minimum Sharpe ratio required during in-sample optimization. Combinations below this are rejected."),
4698
- minProfitFactor: z6.number().min(0).optional().describe("Minimum profit factor required during in-sample optimization. Combinations below this are rejected."),
4699
- requirePositiveNetPl: z6.boolean().default(false).describe("Require positive net P&L during in-sample optimization. Reject combinations with losses."),
4656
+ minSharpeRatio: z6.number().optional().describe(
4657
+ "Minimum Sharpe ratio required during in-sample optimization. Combinations below this are rejected."
4658
+ ),
4659
+ minProfitFactor: z6.number().min(0).optional().describe(
4660
+ "Minimum profit factor required during in-sample optimization. Combinations below this are rejected."
4661
+ ),
4662
+ requirePositiveNetPl: z6.boolean().default(false).describe(
4663
+ "Require positive net P&L during in-sample optimization. Reject combinations with losses."
4664
+ ),
4700
4665
  // Diversification constraints
4701
- enableCorrelationConstraint: z6.boolean().default(false).describe("Enable correlation constraint to reject highly correlated strategy combinations during optimization."),
4702
- maxCorrelationThreshold: z6.number().min(0).max(1).default(0.7).describe("Maximum allowed correlation between any strategy pair (default: 0.7). Only used if enableCorrelationConstraint is true."),
4666
+ enableCorrelationConstraint: z6.boolean().default(false).describe(
4667
+ "Enable correlation constraint to reject highly correlated strategy combinations during optimization."
4668
+ ),
4669
+ maxCorrelationThreshold: z6.number().min(0).max(1).default(0.7).describe(
4670
+ "Maximum allowed correlation between any strategy pair (default: 0.7). Only used if enableCorrelationConstraint is true."
4671
+ ),
4703
4672
  correlationMethod: z6.enum(["kendall", "spearman", "pearson"]).default("kendall").describe("Correlation method for diversification constraint (default: kendall)."),
4704
- enableTailRiskConstraint: z6.boolean().default(false).describe("Enable tail risk constraint to reject combinations with high joint tail dependence."),
4705
- maxTailDependenceThreshold: z6.number().min(0).max(1).default(0.5).describe("Maximum allowed tail dependence between any strategy pair (default: 0.5). Only used if enableTailRiskConstraint is true."),
4706
- tailThreshold: z6.number().min(0.01).max(0.5).default(0.1).describe("Percentile threshold for tail definition (default: 0.1 = worst 10%). Only used if enableTailRiskConstraint is true."),
4673
+ enableTailRiskConstraint: z6.boolean().default(false).describe(
4674
+ "Enable tail risk constraint to reject combinations with high joint tail dependence."
4675
+ ),
4676
+ maxTailDependenceThreshold: z6.number().min(0).max(1).default(0.5).describe(
4677
+ "Maximum allowed tail dependence between any strategy pair (default: 0.5). Only used if enableTailRiskConstraint is true."
4678
+ ),
4679
+ tailThreshold: z6.number().min(0.01).max(0.5).default(0.1).describe(
4680
+ "Percentile threshold for tail definition (default: 0.1 = worst 10%). Only used if enableTailRiskConstraint is true."
4681
+ ),
4707
4682
  diversificationNormalization: z6.enum(["raw", "margin", "notional"]).default("raw").describe("How to normalize returns for diversification calculations (default: raw)."),
4708
4683
  diversificationDateBasis: z6.enum(["opened", "closed"]).default("opened").describe("Which trade date to use for diversification calculations (default: opened)."),
4709
4684
  // Parameter ranges for position sizing sweeps
4710
- parameterRanges: z6.record(
4711
- z6.string(),
4712
- z6.array(z6.number()).min(3).max(3)
4713
- ).optional().describe(
4685
+ parameterRanges: z6.record(z6.string(), z6.array(z6.number()).min(3).max(3)).optional().describe(
4714
4686
  `Parameter ranges for optimization sweep. Format: {paramName: [min, max, step]}. POSITION SIZING: 'kellyMultiplier' scales P&L by multiplier (e.g., {"kellyMultiplier": [0.25, 1.0, 0.25]} tests quarter/half/3-quarter/full Kelly); 'fixedFractionPct' scales relative to 2% baseline (e.g., [1, 4, 1] tests 1-4%); 'fixedContracts' scales relative to avg contracts (e.g., [1, 5, 1] tests 1-5 contracts). RISK CONSTRAINTS (reject combinations exceeding threshold): 'maxDrawdownPct' max drawdown % (e.g., [15, 25, 5] allows 15-25%); 'maxDailyLossPct' max single-day loss %; 'consecutiveLossLimit' max consecutive losing trades. STRATEGY WEIGHTS: 'strategy:StrategyName' weight multiplier per strategy (e.g., {"strategy:IronCondor": [0, 1, 0.5], "strategy:Straddle": [0, 1, 0.5]} tests include/exclude combinations). Multiple parameters create a grid search across all combinations.`
4715
4687
  )
4716
4688
  })
@@ -4750,18 +4722,12 @@ function registerAnalysisTools(server, baseDir) {
4750
4722
  trades = filterByStrategy2(trades, strategy);
4751
4723
  if (tickerFilter) {
4752
4724
  const tickerLower = tickerFilter.toLowerCase();
4753
- trades = trades.filter(
4754
- (t) => resolveTradeTicker(t).toLowerCase() === tickerLower
4755
- );
4725
+ trades = trades.filter((t) => resolveTradeTicker(t).toLowerCase() === tickerLower);
4756
4726
  }
4757
4727
  trades = filterByDateRange(trades, dateRangeFrom, dateRangeTo);
4758
4728
  if (selectedStrategies && selectedStrategies.length > 0) {
4759
- const strategySet = new Set(
4760
- selectedStrategies.map((s) => s.toLowerCase())
4761
- );
4762
- trades = trades.filter(
4763
- (t) => strategySet.has(t.strategy.toLowerCase())
4764
- );
4729
+ const strategySet = new Set(selectedStrategies.map((s) => s.toLowerCase()));
4730
+ trades = trades.filter((t) => strategySet.has(t.strategy.toLowerCase()));
4765
4731
  }
4766
4732
  if (trades.length < 20) {
4767
4733
  return {
@@ -4778,9 +4744,7 @@ function registerAnalysisTools(server, baseDir) {
4778
4744
  (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
4779
4745
  );
4780
4746
  const firstDate = new Date(sortedTrades[0].dateOpened);
4781
- const lastDate = new Date(
4782
- sortedTrades[sortedTrades.length - 1].dateOpened
4783
- );
4747
+ const lastDate = new Date(sortedTrades[sortedTrades.length - 1].dateOpened);
4784
4748
  const totalDays = Math.ceil(
4785
4749
  (lastDate.getTime() - firstDate.getTime()) / (24 * 60 * 60 * 1e3)
4786
4750
  );
@@ -5114,9 +5078,15 @@ function registerAnalysisTools(server, baseDir) {
5114
5078
  minSamples: z6.number().min(2).default(10).describe("Minimum shared trading periods required for valid correlation (default: 10)"),
5115
5079
  strategyFilter: z6.array(z6.string()).optional().describe("Filter to specific strategies only (default: all strategies)"),
5116
5080
  tickerFilter: z6.string().optional().describe("Filter trades by underlying ticker symbol (e.g., 'SPY', 'AAPL')"),
5117
- dateRangeFrom: z6.string().optional().describe("Start date for analysis (ISO format: YYYY-MM-DD). Only include trades on or after this date."),
5118
- dateRangeTo: z6.string().optional().describe("End date for analysis (ISO format: YYYY-MM-DD). Only include trades on or before this date."),
5119
- highlightThreshold: z6.number().min(0).max(1).default(0.7).describe("Threshold for highlighting highly correlated pairs (default: 0.7 = |r| > 0.7)")
5081
+ dateRangeFrom: z6.string().optional().describe(
5082
+ "Start date for analysis (ISO format: YYYY-MM-DD). Only include trades on or after this date."
5083
+ ),
5084
+ dateRangeTo: z6.string().optional().describe(
5085
+ "End date for analysis (ISO format: YYYY-MM-DD). Only include trades on or before this date."
5086
+ ),
5087
+ highlightThreshold: z6.number().min(0).max(1).default(0.7).describe(
5088
+ "Threshold for highlighting highly correlated pairs (default: 0.7 = |r| > 0.7)"
5089
+ )
5120
5090
  })
5121
5091
  },
5122
5092
  async ({
@@ -5138,18 +5108,14 @@ function registerAnalysisTools(server, baseDir) {
5138
5108
  let trades = block.trades;
5139
5109
  if (tickerFilter) {
5140
5110
  const tickerLower = tickerFilter.toLowerCase();
5141
- trades = trades.filter(
5142
- (t) => resolveTradeTicker(t).toLowerCase() === tickerLower
5143
- );
5111
+ trades = trades.filter((t) => resolveTradeTicker(t).toLowerCase() === tickerLower);
5144
5112
  }
5145
5113
  trades = filterByDateRange(trades, dateRangeFrom, dateRangeTo);
5146
5114
  if (strategyFilter && strategyFilter.length > 0) {
5147
5115
  const strategySet = new Set(strategyFilter.map((s) => s.toLowerCase()));
5148
5116
  trades = trades.filter((t) => strategySet.has(t.strategy.toLowerCase()));
5149
5117
  }
5150
- const strategies = Array.from(
5151
- new Set(trades.map((t) => t.strategy))
5152
- ).filter(Boolean);
5118
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).filter(Boolean);
5153
5119
  if (strategies.length < 2) {
5154
5120
  return {
5155
5121
  content: [
@@ -5251,8 +5217,12 @@ function registerAnalysisTools(server, baseDir) {
5251
5217
  dateBasis: z6.enum(["opened", "closed"]).default("opened").describe("Which trade date to use for grouping"),
5252
5218
  strategyFilter: z6.array(z6.string()).optional().describe("Filter to specific strategies only (default: all strategies)"),
5253
5219
  tickerFilter: z6.string().optional().describe("Filter trades by underlying ticker symbol (e.g., 'SPY', 'AAPL')"),
5254
- dateRangeFrom: z6.string().optional().describe("Start date for analysis (ISO format: YYYY-MM-DD). Only include trades on or after this date."),
5255
- dateRangeTo: z6.string().optional().describe("End date for analysis (ISO format: YYYY-MM-DD). Only include trades on or before this date."),
5220
+ dateRangeFrom: z6.string().optional().describe(
5221
+ "Start date for analysis (ISO format: YYYY-MM-DD). Only include trades on or after this date."
5222
+ ),
5223
+ dateRangeTo: z6.string().optional().describe(
5224
+ "End date for analysis (ISO format: YYYY-MM-DD). Only include trades on or before this date."
5225
+ ),
5256
5226
  varianceThreshold: z6.number().min(0.5).max(0.99).default(0.8).describe(
5257
5227
  "Variance threshold for determining effective factors (0.8 = 80% variance explained)"
5258
5228
  )
@@ -5273,9 +5243,7 @@ function registerAnalysisTools(server, baseDir) {
5273
5243
  try {
5274
5244
  const block = await loadBlock(baseDir, blockId);
5275
5245
  const trades = block.trades;
5276
- const strategies = Array.from(
5277
- new Set(trades.map((t) => t.strategy))
5278
- ).filter(Boolean);
5246
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).filter(Boolean);
5279
5247
  if (strategies.length < 2) {
5280
5248
  return {
5281
5249
  content: [
@@ -5355,7 +5323,9 @@ function registerAnalysisTools(server, baseDir) {
5355
5323
  inputSchema: z6.object({
5356
5324
  blockId: z6.string().describe("Block folder name"),
5357
5325
  capitalBase: z6.number().positive().describe("Starting capital in dollars"),
5358
- strategy: z6.string().optional().describe("Filter to a specific strategy name (case-insensitive). If provided, only calculates Kelly for that strategy."),
5326
+ strategy: z6.string().optional().describe(
5327
+ "Filter to a specific strategy name (case-insensitive). If provided, only calculates Kelly for that strategy."
5328
+ ),
5359
5329
  kellyFraction: z6.enum(["full", "half", "quarter"]).default("half").describe(
5360
5330
  "Kelly fraction to use: 'full' (100%), 'half' (50%, recommended), 'quarter' (25%, conservative)"
5361
5331
  ),
@@ -5366,8 +5336,12 @@ function registerAnalysisTools(server, baseDir) {
5366
5336
  useMarginReturns: z6.boolean().default(false).describe(
5367
5337
  "Prefer percentage returns based on margin requirement instead of absolute P&L. More appropriate for compounding strategies with variable position sizes."
5368
5338
  ),
5369
- minTrades: z6.number().min(1).default(10).describe("Minimum trades required per strategy for valid Kelly calculation (default: 10)"),
5370
- sortBy: z6.enum(["name", "kelly", "winRate", "payoffRatio", "allocation"]).default("kelly").describe("Sort strategies by: 'name', 'kelly' percentage, 'winRate', 'payoffRatio', or 'allocation' amount"),
5339
+ minTrades: z6.number().min(1).default(10).describe(
5340
+ "Minimum trades required per strategy for valid Kelly calculation (default: 10)"
5341
+ ),
5342
+ sortBy: z6.enum(["name", "kelly", "winRate", "payoffRatio", "allocation"]).default("kelly").describe(
5343
+ "Sort strategies by: 'name', 'kelly' percentage, 'winRate', 'payoffRatio', or 'allocation' amount"
5344
+ ),
5371
5345
  sortOrder: z6.enum(["asc", "desc"]).default("desc").describe("Sort direction: 'asc' (ascending) or 'desc' (descending)")
5372
5346
  })
5373
5347
  },
@@ -5472,7 +5446,12 @@ function registerAnalysisTools(server, baseDir) {
5472
5446
  }
5473
5447
  }
5474
5448
  const kellyDisplay = portfolioKelly.hasValidKelly ? formatPercent(portfolioKelly.percent) : "N/A";
5475
- const allocDisplay = portfolioKelly.hasValidKelly ? formatCurrency(capitalBase * Math.max(0, Math.min(portfolioKelly.fraction * kellyMultiplier, maxAllocationFraction))) : "N/A";
5449
+ const allocDisplay = portfolioKelly.hasValidKelly ? formatCurrency(
5450
+ capitalBase * Math.max(
5451
+ 0,
5452
+ Math.min(portfolioKelly.fraction * kellyMultiplier, maxAllocationFraction)
5453
+ )
5454
+ ) : "N/A";
5476
5455
  const summary = `Position Sizing: ${blockId}${strategy ? ` (${strategy})` : ""} | Kelly: ${kellyDisplay} | ${kellyFraction} allocation: ${allocDisplay} | ${strategyResults.length} strategies`;
5477
5456
  const structuredData = {
5478
5457
  blockId,
@@ -5498,7 +5477,10 @@ function registerAnalysisTools(server, baseDir) {
5498
5477
  rawKellyPercent: portfolioKelly.percent,
5499
5478
  hasValidKelly: portfolioKelly.hasValidKelly,
5500
5479
  adjustedKellyFraction: portfolioKelly.hasValidKelly ? Math.min(portfolioKelly.fraction * kellyMultiplier, maxAllocationFraction) : null,
5501
- recommendedAllocation: portfolioKelly.hasValidKelly ? capitalBase * Math.max(0, Math.min(portfolioKelly.fraction * kellyMultiplier, maxAllocationFraction)) : null,
5480
+ recommendedAllocation: portfolioKelly.hasValidKelly ? capitalBase * Math.max(
5481
+ 0,
5482
+ Math.min(portfolioKelly.fraction * kellyMultiplier, maxAllocationFraction)
5483
+ ) : null,
5502
5484
  fullKelly: portfolioKelly.hasValidKelly ? Math.min(portfolioKelly.fraction, maxAllocationFraction) : null,
5503
5485
  halfKelly: portfolioKelly.hasValidKelly ? Math.min(portfolioKelly.fraction / 2, maxAllocationFraction) : null,
5504
5486
  quarterKelly: portfolioKelly.hasValidKelly ? Math.min(portfolioKelly.fraction / 4, maxAllocationFraction) : null,
@@ -5509,25 +5491,27 @@ function registerAnalysisTools(server, baseDir) {
5509
5491
  calculationMethod: portfolioKelly.calculationMethod ?? null,
5510
5492
  hasUnrealisticValues: portfolioKelly.hasUnrealisticValues ?? false
5511
5493
  },
5512
- strategies: strategyResults.map(({ name, kelly, rawAllocation, adjustedAllocation, tradeCount }) => ({
5513
- name,
5514
- tradeCount,
5515
- winRate: kelly.winRate,
5516
- avgWin: kelly.avgWin,
5517
- avgLoss: kelly.avgLoss,
5518
- payoffRatio: kelly.payoffRatio,
5519
- rawKellyFraction: kelly.fraction,
5520
- rawKellyPercent: kelly.percent,
5521
- hasValidKelly: kelly.hasValidKelly,
5522
- rawAllocation,
5523
- adjustedAllocation,
5524
- // Margin-based metrics
5525
- avgWinPct: kelly.avgWinPct ?? null,
5526
- avgLossPct: kelly.avgLossPct ?? null,
5527
- normalizedKellyPct: kelly.normalizedKellyPct ?? null,
5528
- calculationMethod: kelly.calculationMethod ?? null,
5529
- hasUnrealisticValues: kelly.hasUnrealisticValues ?? false
5530
- })),
5494
+ strategies: strategyResults.map(
5495
+ ({ name, kelly, rawAllocation, adjustedAllocation, tradeCount }) => ({
5496
+ name,
5497
+ tradeCount,
5498
+ winRate: kelly.winRate,
5499
+ avgWin: kelly.avgWin,
5500
+ avgLoss: kelly.avgLoss,
5501
+ payoffRatio: kelly.payoffRatio,
5502
+ rawKellyFraction: kelly.fraction,
5503
+ rawKellyPercent: kelly.percent,
5504
+ hasValidKelly: kelly.hasValidKelly,
5505
+ rawAllocation,
5506
+ adjustedAllocation,
5507
+ // Margin-based metrics
5508
+ avgWinPct: kelly.avgWinPct ?? null,
5509
+ avgLossPct: kelly.avgLossPct ?? null,
5510
+ normalizedKellyPct: kelly.normalizedKellyPct ?? null,
5511
+ calculationMethod: kelly.calculationMethod ?? null,
5512
+ hasUnrealisticValues: kelly.hasUnrealisticValues ?? false
5513
+ })
5514
+ ),
5531
5515
  skippedStrategies,
5532
5516
  warnings
5533
5517
  };
@@ -5651,14 +5635,10 @@ function createExcursionDistribution(dataPoints, bucketSize = 10) {
5651
5635
  }
5652
5636
  function filterByStrategy3(trades, strategy) {
5653
5637
  if (!strategy) return trades;
5654
- return trades.filter(
5655
- (t) => t.strategy.toLowerCase() === strategy.toLowerCase()
5656
- );
5638
+ return trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
5657
5639
  }
5658
5640
  function getISOWeekNumber(date) {
5659
- const d = new Date(
5660
- Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
5661
- );
5641
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
5662
5642
  const dayNum = d.getUTCDay() || 7;
5663
5643
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
5664
5644
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
@@ -5749,15 +5729,7 @@ function buildReturnDistribution(trades, bucketCount = 20) {
5749
5729
  return buckets;
5750
5730
  }
5751
5731
  function buildDayOfWeekData(trades) {
5752
- const dayNames = [
5753
- "Monday",
5754
- "Tuesday",
5755
- "Wednesday",
5756
- "Thursday",
5757
- "Friday",
5758
- "Saturday",
5759
- "Sunday"
5760
- ];
5732
+ const dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
5761
5733
  const dayData = {};
5762
5734
  trades.forEach((trade) => {
5763
5735
  const date = new Date(trade.dateOpened);
@@ -6200,15 +6172,9 @@ function registerPerformanceTools(server, baseDir) {
6200
6172
  normalizeTo1Lot: z7.boolean().default(false).describe(
6201
6173
  "Normalize all trades to 1 contract for fair comparison across different position sizes"
6202
6174
  ),
6203
- bucketCount: z7.number().min(5).max(100).default(20).describe(
6204
- "Number of histogram buckets for return_distribution (default: 20)"
6205
- ),
6206
- rollingWindowSize: z7.number().min(10).max(100).default(30).describe(
6207
- "Window size for rolling_metrics calculation (default: 30 trades)"
6208
- ),
6209
- mfeMaeBucketSize: z7.number().min(1).max(50).default(10).describe(
6210
- "Bucket size (in %) for MFE/MAE distribution histogram (default: 10%)"
6211
- ),
6175
+ bucketCount: z7.number().min(5).max(100).default(20).describe("Number of histogram buckets for return_distribution (default: 20)"),
6176
+ rollingWindowSize: z7.number().min(10).max(100).default(30).describe("Window size for rolling_metrics calculation (default: 30 trades)"),
6177
+ mfeMaeBucketSize: z7.number().min(1).max(50).default(10).describe("Bucket size (in %) for MFE/MAE distribution histogram (default: 10%)"),
6212
6178
  maxDataPoints: z7.number().min(50).max(1e4).default(500).describe(
6213
6179
  "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."
6214
6180
  )
@@ -6229,7 +6195,11 @@ function registerPerformanceTools(server, baseDir) {
6229
6195
  let truncateArray2 = function(arr) {
6230
6196
  if (arr.length <= maxDataPoints) return arr;
6231
6197
  anyTruncated = true;
6232
- return { data: arr.slice(0, maxDataPoints), truncated: true, totalPoints: arr.length };
6198
+ return {
6199
+ data: arr.slice(0, maxDataPoints),
6200
+ truncated: true,
6201
+ totalPoints: arr.length
6202
+ };
6233
6203
  }, outputLength2 = function(result) {
6234
6204
  return Array.isArray(result) ? result.length : result.data.length;
6235
6205
  };
@@ -6285,10 +6255,7 @@ function registerPerformanceTools(server, baseDir) {
6285
6255
  }
6286
6256
  }
6287
6257
  if (charts.includes("return_distribution")) {
6288
- chartData.returnDistribution = buildReturnDistribution(
6289
- trades,
6290
- bucketCount
6291
- );
6258
+ chartData.returnDistribution = buildReturnDistribution(trades, bucketCount);
6292
6259
  dataPoints += chartData.returnDistribution.length;
6293
6260
  }
6294
6261
  if (charts.includes("day_of_week")) {
@@ -6312,10 +6279,7 @@ function registerPerformanceTools(server, baseDir) {
6312
6279
  dataPoints += outputLength2(result);
6313
6280
  }
6314
6281
  if (charts.includes("rolling_metrics")) {
6315
- chartData.rollingMetrics = buildRollingMetrics(
6316
- trades,
6317
- rollingWindowSize
6318
- );
6282
+ chartData.rollingMetrics = buildRollingMetrics(trades, rollingWindowSize);
6319
6283
  dataPoints += chartData.rollingMetrics.length;
6320
6284
  }
6321
6285
  if (charts.includes("exit_reason_breakdown")) {
@@ -6370,7 +6334,11 @@ function registerPerformanceTools(server, baseDir) {
6370
6334
  let mfeOutputCount;
6371
6335
  if (simplifiedData.length > maxDataPoints) {
6372
6336
  anyTruncated = true;
6373
- mfeDataPointsOutput = { data: simplifiedData.slice(0, maxDataPoints), truncated: true, totalPoints: simplifiedData.length };
6337
+ mfeDataPointsOutput = {
6338
+ data: simplifiedData.slice(0, maxDataPoints),
6339
+ truncated: true,
6340
+ totalPoints: simplifiedData.length
6341
+ };
6374
6342
  mfeOutputCount = maxDataPoints;
6375
6343
  } else {
6376
6344
  mfeDataPointsOutput = simplifiedData;
@@ -6414,9 +6382,7 @@ function registerPerformanceTools(server, baseDir) {
6414
6382
  const filters = [];
6415
6383
  if (strategy) filters.push(`strategy=${strategy}`);
6416
6384
  if (dateRange?.from || dateRange?.to) {
6417
- filters.push(
6418
- `date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
6419
- );
6385
+ filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
6420
6386
  }
6421
6387
  if (normalizeTo1Lot) filters.push("normalized");
6422
6388
  const filterStr = filters.length > 0 ? ` (${filters.join(", ")})` : "";
@@ -6462,9 +6428,7 @@ function registerPerformanceTools(server, baseDir) {
6462
6428
  from: z7.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
6463
6429
  to: z7.string().optional().describe("End date YYYY-MM-DD (inclusive)")
6464
6430
  }).optional().describe("Filter trades to date range (takes precedence over year)"),
6465
- normalizeTo1Lot: z7.boolean().default(false).describe(
6466
- "Normalize all trades to 1 contract for fair comparison"
6467
- )
6431
+ normalizeTo1Lot: z7.boolean().default(false).describe("Normalize all trades to 1 contract for fair comparison")
6468
6432
  })
6469
6433
  },
6470
6434
  async ({ blockId, strategy, period, year, dateRange, normalizeTo1Lot }) => {
@@ -6475,9 +6439,7 @@ function registerPerformanceTools(server, baseDir) {
6475
6439
  if (dateRange) {
6476
6440
  trades = filterByDateRange2(trades, dateRange.from, dateRange.to);
6477
6441
  } else if (year !== void 0) {
6478
- trades = trades.filter(
6479
- (t) => new Date(t.dateOpened).getFullYear() === year
6480
- );
6442
+ trades = trades.filter((t) => new Date(t.dateOpened).getFullYear() === year);
6481
6443
  }
6482
6444
  if (normalizeTo1Lot) {
6483
6445
  trades = normalizeToOneLot(trades);
@@ -6534,9 +6496,7 @@ function registerPerformanceTools(server, baseDir) {
6534
6496
  const filters = [];
6535
6497
  if (strategy) filters.push(`strategy=${strategy}`);
6536
6498
  if (dateRange?.from || dateRange?.to) {
6537
- filters.push(
6538
- `date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
6539
- );
6499
+ filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
6540
6500
  } else if (year !== void 0) {
6541
6501
  filters.push(`year=${year}`);
6542
6502
  }
@@ -6591,18 +6551,24 @@ function registerPerformanceTools(server, baseDir) {
6591
6551
  detailLevel: z7.enum(["summary", "trades"]).default("summary").describe(
6592
6552
  "'summary' (default): aggregate by date+strategy. 'trades': individual trade comparison with field-by-field differences"
6593
6553
  ),
6594
- outliersOnly: z7.boolean().default(false).describe(
6595
- "Only return high-slippage outliers (trades exceeding z-score threshold)"
6596
- ),
6597
- outliersThreshold: z7.number().default(2).describe(
6598
- "Z-score threshold for outlier detection (default: 2 = ~95% confidence)"
6599
- ),
6554
+ outliersOnly: z7.boolean().default(false).describe("Only return high-slippage outliers (trades exceeding z-score threshold)"),
6555
+ outliersThreshold: z7.number().default(2).describe("Z-score threshold for outlier detection (default: 2 = ~95% confidence)"),
6600
6556
  groupBy: z7.enum(["none", "strategy", "date", "week", "month"]).default("none").describe(
6601
6557
  "Group results: 'none' (flat list), 'strategy', 'date' (daily), 'week', 'month'"
6602
6558
  )
6603
6559
  })
6604
6560
  },
6605
- async ({ blockId, strategy, scaling, dateRange, matchedOnly, detailLevel, outliersOnly, outliersThreshold, groupBy }) => {
6561
+ async ({
6562
+ blockId,
6563
+ strategy,
6564
+ scaling,
6565
+ dateRange,
6566
+ matchedOnly,
6567
+ detailLevel,
6568
+ outliersOnly,
6569
+ outliersThreshold,
6570
+ groupBy
6571
+ }) => {
6606
6572
  try {
6607
6573
  const block = await loadBlock(baseDir, blockId);
6608
6574
  let backtestTrades = block.trades;
@@ -6966,10 +6932,7 @@ function registerPerformanceTools(server, baseDir) {
6966
6932
  const slippageValues = matchedComparisons.map((c) => c.slippage);
6967
6933
  if (slippageValues.length >= 3) {
6968
6934
  const meanSlippage = slippageValues.reduce((sum, v) => sum + v, 0) / slippageValues.length;
6969
- const variance = slippageValues.reduce(
6970
- (sum, v) => sum + Math.pow(v - meanSlippage, 2),
6971
- 0
6972
- ) / slippageValues.length;
6935
+ const variance = slippageValues.reduce((sum, v) => sum + Math.pow(v - meanSlippage, 2), 0) / slippageValues.length;
6973
6936
  const stdDevSlippage = Math.sqrt(variance);
6974
6937
  if (stdDevSlippage >= 1e-10) {
6975
6938
  for (const comparison of comparisons) {
@@ -6989,10 +6952,7 @@ function registerPerformanceTools(server, baseDir) {
6989
6952
  }
6990
6953
  }
6991
6954
  const outliers = comparisons.filter((c) => c.isOutlier);
6992
- const outlierTotalSlippage = outliers.reduce(
6993
- (sum, c) => sum + c.slippage,
6994
- 0
6995
- );
6955
+ const outlierTotalSlippage = outliers.reduce((sum, c) => sum + c.slippage, 0);
6996
6956
  outlierStats = {
6997
6957
  meanSlippage,
6998
6958
  stdDevSlippage,
@@ -7007,9 +6967,7 @@ function registerPerformanceTools(server, baseDir) {
7007
6967
  const unmatchedBacktestEntries = comparisons.filter(
7008
6968
  (c) => !c.matched && c.backtestPl !== 0
7009
6969
  );
7010
- const unmatchedActualEntries = comparisons.filter(
7011
- (c) => !c.matched && c.actualPl !== 0
7012
- );
6970
+ const unmatchedActualEntries = comparisons.filter((c) => !c.matched && c.actualPl !== 0);
7013
6971
  const unmatchedBacktestSummary = unmatchedBacktestEntries.length > 0 ? {
7014
6972
  count: unmatchedBacktestEntries.length,
7015
6973
  dateRange: {
@@ -7017,7 +6975,9 @@ function registerPerformanceTools(server, baseDir) {
7017
6975
  to: unmatchedBacktestEntries.reduce((a, b) => a.date > b.date ? a : b).date
7018
6976
  },
7019
6977
  totalPl: unmatchedBacktestEntries.reduce((sum, c) => sum + c.backtestPl, 0),
7020
- strategies: Array.from(new Set(unmatchedBacktestEntries.map((c) => c.strategy))).sort()
6978
+ strategies: Array.from(
6979
+ new Set(unmatchedBacktestEntries.map((c) => c.strategy))
6980
+ ).sort()
7021
6981
  } : null;
7022
6982
  const unmatchedActualSummary = unmatchedActualEntries.length > 0 ? {
7023
6983
  count: unmatchedActualEntries.length,
@@ -7026,15 +6986,15 @@ function registerPerformanceTools(server, baseDir) {
7026
6986
  to: unmatchedActualEntries.reduce((a, b) => a.date > b.date ? a : b).date
7027
6987
  },
7028
6988
  totalPl: unmatchedActualEntries.reduce((sum, c) => sum + c.actualPl, 0),
7029
- strategies: Array.from(new Set(unmatchedActualEntries.map((c) => c.strategy))).sort()
6989
+ strategies: Array.from(
6990
+ new Set(unmatchedActualEntries.map((c) => c.strategy))
6991
+ ).sort()
7030
6992
  } : null;
7031
6993
  let outputComparisons = matchedOnly ? comparisons.filter((c) => c.matched) : [...comparisons];
7032
6994
  if (outliersOnly) {
7033
6995
  outputComparisons = outputComparisons.filter((c) => c.isOutlier);
7034
6996
  }
7035
- outputComparisons.sort(
7036
- (a, b) => Math.abs(b.slippage) - Math.abs(a.slippage)
7037
- );
6997
+ outputComparisons.sort((a, b) => Math.abs(b.slippage) - Math.abs(a.slippage));
7038
6998
  let groups = null;
7039
6999
  if (groupBy !== "none") {
7040
7000
  const groupMap = /* @__PURE__ */ new Map();
@@ -7046,13 +7006,8 @@ function registerPerformanceTools(server, baseDir) {
7046
7006
  }
7047
7007
  groups = Array.from(groupMap.entries()).map(([groupKey, groupComparisons]) => {
7048
7008
  const matchedInGroup = groupComparisons.filter((c) => c.matched);
7049
- const totalSlippage2 = groupComparisons.reduce(
7050
- (sum, c) => sum + c.slippage,
7051
- 0
7052
- );
7053
- const outlierCount = groupComparisons.filter(
7054
- (c) => c.isOutlier
7055
- ).length;
7009
+ const totalSlippage2 = groupComparisons.reduce((sum, c) => sum + c.slippage, 0);
7010
+ const outlierCount = groupComparisons.filter((c) => c.isOutlier).length;
7056
7011
  return {
7057
7012
  groupKey,
7058
7013
  count: groupComparisons.length,
@@ -7080,15 +7035,11 @@ function registerPerformanceTools(server, baseDir) {
7080
7035
  const backtestStrategies = Array.from(
7081
7036
  new Set(backtestTrades.map((t) => t.strategy))
7082
7037
  ).sort();
7083
- const actualStrategies = Array.from(
7084
- new Set(actualTrades.map((t) => t.strategy))
7085
- ).sort();
7038
+ const actualStrategies = Array.from(new Set(actualTrades.map((t) => t.strategy))).sort();
7086
7039
  const filters = [];
7087
7040
  if (strategy) filters.push(`strategy=${strategy}`);
7088
7041
  if (dateRange?.from || dateRange?.to) {
7089
- filters.push(
7090
- `date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
7091
- );
7042
+ filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
7092
7043
  }
7093
7044
  if (autoFilterApplied) filters.push("auto-date-overlap");
7094
7045
  if (matchedOnly) filters.push("matched-only");
@@ -7196,9 +7147,7 @@ function getISOWeekNumber2(date) {
7196
7147
  const dayNum = d.getDay() || 7;
7197
7148
  d.setDate(d.getDate() + 4 - dayNum);
7198
7149
  const yearStart = new Date(d.getFullYear(), 0, 1);
7199
- const weekNo = Math.ceil(
7200
- ((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
7201
- );
7150
+ const weekNo = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
7202
7151
  return weekNo;
7203
7152
  }
7204
7153
  function enrichTrades(trades) {
@@ -7317,10 +7266,7 @@ function generateHistogram(values, bucketCount = 10) {
7317
7266
  buckets.push({ min: bucketMin, max: bucketMax, count: 0 });
7318
7267
  }
7319
7268
  for (const value of values) {
7320
- const bucketIndex = Math.min(
7321
- Math.floor((value - min) / bucketSize),
7322
- bucketCount - 1
7323
- );
7269
+ const bucketIndex = Math.min(Math.floor((value - min) / bucketSize), bucketCount - 1);
7324
7270
  if (bucketIndex >= 0 && bucketIndex < buckets.length) {
7325
7271
  buckets[bucketIndex].count++;
7326
7272
  }
@@ -7336,9 +7282,7 @@ function registerFieldTools(server, baseDir) {
7336
7282
  description: "Get detailed statistics for a specific field including min/max/avg/median/stdDev, percentiles, and histogram",
7337
7283
  inputSchema: z8.object({
7338
7284
  blockId: z8.string().describe("Block folder name"),
7339
- field: z8.string().describe(
7340
- "Field name to analyze (e.g., 'openingVix', 'pl', 'rom', 'mfePercent')"
7341
- ),
7285
+ field: z8.string().describe("Field name to analyze (e.g., 'openingVix', 'pl', 'rom', 'mfePercent')"),
7342
7286
  strategy: z8.string().optional().describe("Filter by strategy name (case-insensitive)"),
7343
7287
  startDate: z8.string().optional().describe("Filter by start date (YYYY-MM-DD)"),
7344
7288
  endDate: z8.string().optional().describe("Filter by end date (YYYY-MM-DD)"),
@@ -7347,14 +7291,7 @@ function registerFieldTools(server, baseDir) {
7347
7291
  },
7348
7292
  withSyncedBlock(
7349
7293
  baseDir,
7350
- async ({
7351
- blockId,
7352
- field,
7353
- strategy,
7354
- startDate,
7355
- endDate,
7356
- histogramBuckets
7357
- }) => {
7294
+ async ({ blockId, field, strategy, startDate, endDate, histogramBuckets }) => {
7358
7295
  try {
7359
7296
  const block = await loadBlock(baseDir, blockId);
7360
7297
  let trades = block.trades;
@@ -7453,7 +7390,9 @@ function registerPredictiveTools(server, baseDir) {
7453
7390
  inputSchema: z9.object({
7454
7391
  blockId: z9.string().describe("Block folder name"),
7455
7392
  strategy: z9.string().optional().describe("Pre-filter by strategy name (case-insensitive)"),
7456
- strategyName: z9.string().optional().describe("Strategy profile name. When provided, auto-filters to that strategy's trades and adds profile context to output."),
7393
+ strategyName: z9.string().optional().describe(
7394
+ "Strategy profile name. When provided, auto-filters to that strategy's trades and adds profile context to output."
7395
+ ),
7457
7396
  startDate: z9.string().optional().describe("Pre-filter by start date (YYYY-MM-DD)"),
7458
7397
  endDate: z9.string().optional().describe("Pre-filter by end date (YYYY-MM-DD)"),
7459
7398
  targetField: z9.string().default("pl").describe("Field to correlate against (default: 'pl' for profit/loss)"),
@@ -7599,9 +7538,7 @@ function registerPredictiveTools(server, baseDir) {
7599
7538
  const conn = await getConnection(baseDir);
7600
7539
  const profile = await getProfile(conn, blockId, strategyName, baseDir);
7601
7540
  if (profile && profile.entryFilters.length > 0) {
7602
- const profileFilterFields = new Set(
7603
- profile.entryFilters.map((f) => f.field)
7604
- );
7541
+ const profileFilterFields = new Set(profile.entryFilters.map((f) => f.field));
7605
7542
  const alignedFields = rankedFields.filter((rf) => profileFilterFields.has(rf.field)).map((rf) => ({
7606
7543
  field: rf.field,
7607
7544
  correlation: rf.correlation,
@@ -7639,9 +7576,7 @@ function registerPredictiveTools(server, baseDir) {
7639
7576
  description: "Sweep filter thresholds for a field and show performance at each threshold. Use after find_predictive_fields to determine optimal filter values. Returns outcome curves and identifies sweet spots where filtering improves performance.",
7640
7577
  inputSchema: z9.object({
7641
7578
  blockId: z9.string().describe("Block folder name"),
7642
- field: z9.string().describe(
7643
- "Field to sweep thresholds on (e.g., 'openingVix', 'durationHours')"
7644
- ),
7579
+ field: z9.string().describe("Field to sweep thresholds on (e.g., 'openingVix', 'durationHours')"),
7645
7580
  mode: z9.enum(["lt", "gt", "both"]).default("both").describe(
7646
7581
  "Direction of filter: 'lt' (field < threshold), 'gt' (field > threshold), 'both' (show both directions)"
7647
7582
  ),
@@ -7739,9 +7674,7 @@ function registerPredictiveTools(server, baseDir) {
7739
7674
  const baselinePls = tradesWithField.map((t) => t.pl);
7740
7675
  const baselineTotalPl = baselinePls.reduce((a, b) => a + b, 0);
7741
7676
  const baselineAvgPl = baselineTotalPl / tradesWithField.length;
7742
- const baselineWinners = tradesWithField.filter(
7743
- (t) => t.pl > 0
7744
- ).length;
7677
+ const baselineWinners = tradesWithField.filter((t) => t.pl > 0).length;
7745
7678
  const baselineWinRate = baselineWinners / tradesWithField.length;
7746
7679
  const baseline = {
7747
7680
  count: tradesWithField.length,
@@ -7871,9 +7804,7 @@ function registerDiscrepancyTool(server, baseDir) {
7871
7804
  scaling: z10.enum(["raw", "perContract", "toReported"]).default("toReported").describe("Scaling mode for P/L comparison (default: toReported)"),
7872
7805
  correlationMethod: z10.enum(["pearson", "kendall"]).default("pearson").describe("Correlation method for market condition analysis"),
7873
7806
  minSamples: z10.number().min(5).default(10).describe("Minimum samples required for pattern detection"),
7874
- patternThreshold: z10.number().min(0.5).max(0.95).default(0.7).describe(
7875
- "Threshold for detecting systematic patterns (0.7 = 70% consistency)"
7876
- )
7807
+ patternThreshold: z10.number().min(0.5).max(0.95).default(0.7).describe("Threshold for detecting systematic patterns (0.7 = 70% consistency)")
7877
7808
  })
7878
7809
  },
7879
7810
  withSyncedBlock(
@@ -7930,7 +7861,11 @@ function registerDiscrepancyTool(server, baseDir) {
7930
7861
  isError: true
7931
7862
  };
7932
7863
  }
7933
- const { matchedTrades, unmatchedBacktestCount, unmatchedActualCount } = matchTrades(backtestTrades, actualTrades, scaling);
7864
+ const { matchedTrades, unmatchedBacktestCount, unmatchedActualCount } = matchTrades(
7865
+ backtestTrades,
7866
+ actualTrades,
7867
+ scaling
7868
+ );
7934
7869
  if (matchedTrades.length === 0) {
7935
7870
  return {
7936
7871
  content: [
@@ -7998,12 +7933,8 @@ function registerDiscrepancyTool(server, baseDir) {
7998
7933
  (a) => a.totalSlippage < outlierThresholdLow || a.totalSlippage > outlierThresholdHigh
7999
7934
  );
8000
7935
  if (outlierTrades.length >= 3) {
8001
- for (const [bucketName, bucketTrades] of Object.entries(
8002
- buckets
8003
- )) {
8004
- const outlierInBucket = outlierTrades.filter(
8005
- (o) => bucketTrades.includes(o)
8006
- );
7936
+ for (const [bucketName, bucketTrades] of Object.entries(buckets)) {
7937
+ const outlierInBucket = outlierTrades.filter((o) => bucketTrades.includes(o));
8007
7938
  const bucketRate = outlierInBucket.length / outlierTrades.length;
8008
7939
  if (bucketRate >= patternThreshold && outlierInBucket.length >= 3) {
8009
7940
  patterns.push({
@@ -8071,9 +8002,7 @@ function registerDiscrepancyTool(server, baseDir) {
8071
8002
  }
8072
8003
  }
8073
8004
  }
8074
- results.sort(
8075
- (a, b) => Math.abs(b.coefficient) - Math.abs(a.coefficient)
8076
- );
8005
+ results.sort((a, b) => Math.abs(b.coefficient) - Math.abs(a.coefficient));
8077
8006
  return results;
8078
8007
  };
8079
8008
  const portfolioPatterns = detectPatterns(matchedTrades);
@@ -8096,9 +8025,7 @@ function registerDiscrepancyTool(server, baseDir) {
8096
8025
  avgSlippage: stratAvg
8097
8026
  });
8098
8027
  }
8099
- perStrategy.sort(
8100
- (a, b) => Math.abs(b.totalSlippage) - Math.abs(a.totalSlippage)
8101
- );
8028
+ perStrategy.sort((a, b) => Math.abs(b.totalSlippage) - Math.abs(a.totalSlippage));
8102
8029
  const summaryParts = [
8103
8030
  `Slippage analysis: ${matchedTrades.length} matched trades`,
8104
8031
  `Total slippage: ${formatCurrency(totalSlippage)}`,
@@ -8156,15 +8083,9 @@ function registerStrategyMatchesTool(server, baseDir) {
8156
8083
  to: z11.string().optional().describe("End date YYYY-MM-DD")
8157
8084
  }).optional().describe("Filter trades to date range"),
8158
8085
  correlationMethod: z11.enum(["pearson", "spearman", "kendall"]).default("pearson").describe("Correlation method (default: pearson)"),
8159
- minOverlapDays: z11.number().min(2).default(5).describe(
8160
- "Minimum overlapping trading days required for correlation (default: 5)"
8161
- ),
8162
- minCorrelation: z11.number().min(-1).max(1).optional().describe(
8163
- "Minimum correlation to include in suggestions (default: show all)"
8164
- ),
8165
- includeUnmatched: z11.boolean().default(true).describe(
8166
- "Include strategies with no potential matches (default: true)"
8167
- )
8086
+ minOverlapDays: z11.number().min(2).default(5).describe("Minimum overlapping trading days required for correlation (default: 5)"),
8087
+ minCorrelation: z11.number().min(-1).max(1).optional().describe("Minimum correlation to include in suggestions (default: show all)"),
8088
+ includeUnmatched: z11.boolean().default(true).describe("Include strategies with no potential matches (default: true)")
8168
8089
  })
8169
8090
  },
8170
8091
  withSyncedBlock(
@@ -8218,9 +8139,7 @@ function registerStrategyMatchesTool(server, baseDir) {
8218
8139
  isError: true
8219
8140
  };
8220
8141
  }
8221
- const backtestStrategies = new Set(
8222
- backtestTrades.map((t) => t.strategy)
8223
- );
8142
+ const backtestStrategies = new Set(backtestTrades.map((t) => t.strategy));
8224
8143
  const actualStrategies = new Set(actualTrades.map((t) => t.strategy));
8225
8144
  const normalizeStrategyName = (name) => name.toLowerCase().trim();
8226
8145
  const backtestStrategyMap = /* @__PURE__ */ new Map();
@@ -8264,14 +8183,8 @@ function registerStrategyMatchesTool(server, baseDir) {
8264
8183
  }
8265
8184
  return result;
8266
8185
  };
8267
- const backtestDaily = buildDailyPlSeries(
8268
- backtestTrades,
8269
- backtestWithExactMatch
8270
- );
8271
- const actualDaily = buildDailyPlSeries(
8272
- actualTrades,
8273
- actualWithExactMatch
8274
- );
8186
+ const backtestDaily = buildDailyPlSeries(backtestTrades, backtestWithExactMatch);
8187
+ const actualDaily = buildDailyPlSeries(actualTrades, actualWithExactMatch);
8275
8188
  const getNormalizedDailyPl = (dailyMap) => {
8276
8189
  const result = /* @__PURE__ */ new Map();
8277
8190
  for (const [date, data] of dailyMap) {
@@ -8340,10 +8253,7 @@ function registerStrategyMatchesTool(server, baseDir) {
8340
8253
  actualNormalized,
8341
8254
  correlationMethod
8342
8255
  );
8343
- const timingOverlap = calculateTimingOverlap(
8344
- btRawDaily,
8345
- actualRawDaily
8346
- );
8256
+ const timingOverlap = calculateTimingOverlap(btRawDaily, actualRawDaily);
8347
8257
  rowCorrelations.push(isNaN(correlation) ? 0 : correlation);
8348
8258
  rowSampleSizes.push(overlapDays);
8349
8259
  btResults.set(actualStrategy, {
@@ -8402,10 +8312,7 @@ function registerStrategyMatchesTool(server, baseDir) {
8402
8312
  }
8403
8313
  const meanDiff = differences.reduce((a, b) => a + b, 0) / differences.length;
8404
8314
  const stdDiff = Math.sqrt(
8405
- differences.reduce(
8406
- (sum, d) => sum + Math.pow(d - meanDiff, 2),
8407
- 0
8408
- ) / differences.length
8315
+ differences.reduce((sum, d) => sum + Math.pow(d - meanDiff, 2), 0) / differences.length
8409
8316
  );
8410
8317
  const bias = stdDiff > 0 ? Math.abs(meanDiff) / stdDiff : 0;
8411
8318
  if (bias > SYSTEMATIC_BIAS_THRESHOLD) {
@@ -8533,9 +8440,7 @@ function registerSlippageTrendsTool(server, baseDir) {
8533
8440
  }).optional().describe("Filter trades to date range"),
8534
8441
  scaling: z12.enum(["raw", "perContract", "toReported"]).default("toReported").describe("Scaling mode for P/L comparison (default: toReported)"),
8535
8442
  granularity: z12.enum(["daily", "weekly", "monthly"]).default("weekly").describe("Time period granularity for trend analysis"),
8536
- includeTimeSeries: z12.boolean().default(false).describe(
8537
- "Include raw time series data points in output (for charting)"
8538
- ),
8443
+ includeTimeSeries: z12.boolean().default(false).describe("Include raw time series data points in output (for charting)"),
8539
8444
  correlationMethod: z12.enum(["pearson", "kendall"]).default("pearson").describe("Correlation method for external factor analysis"),
8540
8445
  minSamples: z12.number().min(5).default(10).describe("Minimum samples required for reliable statistics")
8541
8446
  })
@@ -8595,11 +8500,7 @@ function registerSlippageTrendsTool(server, baseDir) {
8595
8500
  isError: true
8596
8501
  };
8597
8502
  }
8598
- const { matchedTrades } = matchTrades(
8599
- backtestTrades,
8600
- actualTrades,
8601
- scaling
8602
- );
8503
+ const { matchedTrades } = matchTrades(backtestTrades, actualTrades, scaling);
8603
8504
  if (matchedTrades.length === 0) {
8604
8505
  return {
8605
8506
  content: [
@@ -8659,10 +8560,7 @@ function registerSlippageTrendsTool(server, baseDir) {
8659
8560
  const slope = sumX2 > 0 ? sumXY / sumX2 : 0;
8660
8561
  const intercept = meanY - slope * meanX;
8661
8562
  const predicted = x.map((xi) => slope * xi + intercept);
8662
- const ssRes = y.reduce(
8663
- (sum, yi, i) => sum + (yi - predicted[i]) ** 2,
8664
- 0
8665
- );
8563
+ const ssRes = y.reduce((sum, yi, i) => sum + (yi - predicted[i]) ** 2, 0);
8666
8564
  const ssTot = y.reduce((sum, yi) => sum + (yi - meanY) ** 2, 0);
8667
8565
  const rSquared = ssTot > 0 ? 1 - ssRes / ssTot : 0;
8668
8566
  const mse = n > 2 ? ssRes / (n - 2) : 0;
@@ -8683,10 +8581,7 @@ function registerSlippageTrendsTool(server, baseDir) {
8683
8581
  from: dates[0],
8684
8582
  to: dates[dates.length - 1]
8685
8583
  };
8686
- const totalSlippage = matchedTrades.reduce(
8687
- (sum, t) => sum + t.totalSlippage,
8688
- 0
8689
- );
8584
+ const totalSlippage = matchedTrades.reduce((sum, t) => sum + t.totalSlippage, 0);
8690
8585
  const avgSlippagePerTrade = totalSlippage / matchedTrades.length;
8691
8586
  const avgSlippagePerPeriod = periodSlippages.length > 0 ? periodSlippages.reduce((sum, p) => sum + p.totalSlippage, 0) / periodSlippages.length : 0;
8692
8587
  const periodAvgSlippages = periodSlippages.map((p) => p.avgSlippage);
@@ -8719,9 +8614,7 @@ function registerSlippageTrendsTool(server, baseDir) {
8719
8614
  trend: strategyTrend
8720
8615
  });
8721
8616
  }
8722
- perStrategy.sort(
8723
- (a, b) => Math.abs(b.totalSlippage) - Math.abs(a.totalSlippage)
8724
- );
8617
+ perStrategy.sort((a, b) => Math.abs(b.totalSlippage) - Math.abs(a.totalSlippage));
8725
8618
  let externalFactors;
8726
8619
  const vixTrades = matchedTrades.filter(
8727
8620
  (t) => t.openingVix !== void 0 && t.openingVix !== null
@@ -9060,12 +8953,7 @@ function registerMarketDataTools(server, baseDir, stores) {
9060
8953
  description: "Break down a block's trade performance by market regime using market.enriched + market.spot_daily (including VIX tickers) and market.enriched_context. Identifies which market conditions favor or hurt the strategy. Close-derived fields (volRegime, termStructure) use prior trading day values to prevent lookahead bias. Vol_Regime and Term_Structure_State come from market.enriched_context via JOIN. Returns warnings when market data is partially missing.",
9061
8954
  inputSchema: z14.object({
9062
8955
  blockId: z14.string().describe("Block ID to analyze"),
9063
- segmentBy: z14.enum([
9064
- "volRegime",
9065
- "termStructure",
9066
- "dayOfWeek",
9067
- "gapDirection"
9068
- ]).describe("Market dimension to segment by"),
8956
+ segmentBy: z14.enum(["volRegime", "termStructure", "dayOfWeek", "gapDirection"]).describe("Market dimension to segment by"),
9069
8957
  strategy: z14.string().optional().describe("Filter to specific strategy"),
9070
8958
  ticker: z14.string().optional().describe("Underlying ticker symbol (default: SPX)")
9071
8959
  })
@@ -9075,9 +8963,7 @@ function registerMarketDataTools(server, baseDir, stores) {
9075
8963
  const block = await loadBlock(baseDir, blockId);
9076
8964
  let trades = block.trades;
9077
8965
  if (strategy) {
9078
- trades = trades.filter(
9079
- (t) => t.strategy.toLowerCase() === strategy.toLowerCase()
9080
- );
8966
+ trades = trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
9081
8967
  }
9082
8968
  if (trades.length === 0) {
9083
8969
  return {
@@ -9246,252 +9132,468 @@ function registerMarketDataTools(server, baseDir, stores) {
9246
9132
  inputSchema: z14.object({
9247
9133
  blockId: z14.string().describe("Block ID to analyze"),
9248
9134
  strategy: z14.string().optional().describe("Filter to specific strategy"),
9249
- strategyName: z14.string().optional().describe("Strategy profile name. When provided, auto-filters to that strategy's trades and cross-references suggestions against profile's entry_filters."),
9135
+ strategyName: z14.string().optional().describe(
9136
+ "Strategy profile name. When provided, auto-filters to that strategy's trades and cross-references suggestions against profile's entry_filters."
9137
+ ),
9250
9138
  minImprovementPct: z14.number().optional().describe("Only suggest filters with >= X% win rate improvement (default: 3)"),
9251
9139
  ticker: z14.string().optional().describe("Underlying ticker symbol (default: SPX)")
9252
9140
  })
9253
9141
  },
9254
- withFullSync(baseDir, async ({ blockId, strategy, strategyName, minImprovementPct = 3, ticker }) => {
9255
- try {
9256
- const block = await loadBlock(baseDir, blockId);
9257
- let trades = block.trades;
9258
- const effectiveStrategy = strategyName || strategy;
9259
- if (effectiveStrategy) {
9260
- trades = filterByStrategy(trades, effectiveStrategy);
9261
- if (trades.length === 0 && block.trades.length > 0) {
9262
- const uniqueStrategies = new Set(block.trades.map((t) => t.strategy));
9263
- if (uniqueStrategies.size === 1) {
9264
- trades = block.trades;
9142
+ withFullSync(
9143
+ baseDir,
9144
+ async ({ blockId, strategy, strategyName, minImprovementPct = 3, ticker }) => {
9145
+ try {
9146
+ const block = await loadBlock(baseDir, blockId);
9147
+ let trades = block.trades;
9148
+ const effectiveStrategy = strategyName || strategy;
9149
+ if (effectiveStrategy) {
9150
+ trades = filterByStrategy(trades, effectiveStrategy);
9151
+ if (trades.length === 0 && block.trades.length > 0) {
9152
+ const uniqueStrategies = new Set(block.trades.map((t) => t.strategy));
9153
+ if (uniqueStrategies.size === 1) {
9154
+ trades = block.trades;
9155
+ }
9265
9156
  }
9266
9157
  }
9267
- }
9268
- let profileEntryFilters = null;
9269
- if (strategyName) {
9270
- const conn2 = await getConnection(baseDir);
9271
- const profile = await getProfile(conn2, blockId, strategyName, baseDir);
9272
- if (profile) {
9273
- profileEntryFilters = profile.entryFilters;
9158
+ let profileEntryFilters = null;
9159
+ if (strategyName) {
9160
+ const conn2 = await getConnection(baseDir);
9161
+ const profile = await getProfile(conn2, blockId, strategyName, baseDir);
9162
+ if (profile) {
9163
+ profileEntryFilters = profile.entryFilters;
9164
+ }
9274
9165
  }
9275
- }
9276
- if (trades.length === 0) {
9277
- return {
9278
- content: [{ type: "text", text: "No trades found" }],
9279
- isError: true
9280
- };
9281
- }
9282
- const tradeKeys = uniqueTradeLookupKeys2(trades);
9283
- const conn = await getConnection(baseDir);
9284
- const resolvedTickerSF = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
9285
- const availabilitySF = await checkDataAvailability(stores, resolvedTickerSF);
9286
- const { sql, params } = buildLookaheadFreeQuery(tradeKeys);
9287
- const dailyResult = await conn.runAndReadAll(sql, params);
9288
- const dailyRecords = resultToRecords2(dailyResult);
9289
- const daily = recordsByTickerDate2(dailyRecords);
9290
- const enrichedTrades = trades.map((trade) => {
9291
- const lookup = getTradeLookupKey2(trade);
9292
- return {
9293
- trade,
9294
- market: daily.get(marketTickerDateKey(lookup.ticker, lookup.date)) || null
9295
- };
9296
- });
9297
- const matchedTrades = enrichedTrades.filter((t) => t.market !== null);
9298
- if (matchedTrades.length < 10) {
9299
- return {
9300
- content: [{ type: "text", text: "Not enough trades matched to market data for analysis (need at least 10)" }],
9301
- isError: true
9302
- };
9303
- }
9304
- const currentWins = matchedTrades.filter((t) => t.trade.pl > 0).length;
9305
- const currentWinRate = currentWins / matchedTrades.length * 100;
9306
- const currentTotalPl = matchedTrades.reduce((sum, t) => sum + t.trade.pl, 0);
9307
- const suggestions = [];
9308
- const testFilters = [
9309
- // Open-known filters (same-day values)
9310
- // Gap filters
9311
- { name: "Skip when |Gap_Pct| > 0.5%", field: "Gap_Pct", operator: ">", value: 0.5, test: (m) => Math.abs(getNum2(m, "Gap_Pct")) > 0.5, lagged: false },
9312
- { name: "Skip when |Gap_Pct| > 0.8%", field: "Gap_Pct", operator: ">", value: 0.8, test: (m) => Math.abs(getNum2(m, "Gap_Pct")) > 0.8, lagged: false },
9313
- { name: "Skip when |Gap_Pct| > 1.0%", field: "Gap_Pct", operator: ">", value: 1, test: (m) => Math.abs(getNum2(m, "Gap_Pct")) > 1, lagged: false },
9314
- // Day of week
9315
- { name: "Skip Fridays", field: "Day_of_Week", operator: "==", value: 6, test: (m) => getNum2(m, "Day_of_Week") === 6, lagged: false },
9316
- { name: "Skip Mondays", field: "Day_of_Week", operator: "==", value: 2, test: (m) => getNum2(m, "Day_of_Week") === 2, lagged: false },
9317
- // OPEX
9318
- { name: "Skip OPEX days", field: "Is_Opex", operator: "==", value: 1, test: (m) => getNum2(m, "Is_Opex") === 1, lagged: false },
9319
- // VIX_Open filters (open-known)
9320
- { name: "Skip when VIX_Open > 25", field: "VIX_Open", operator: ">", value: 25, test: (m) => getNum2(m, "VIX_Open") > 25, lagged: false },
9321
- { name: "Skip when VIX_Open > 30", field: "VIX_Open", operator: ">", value: 30, test: (m) => getNum2(m, "VIX_Open") > 30, lagged: false },
9322
- // VIX_Gap_Pct filters (open-known)
9323
- { name: "Skip when |VIX_Gap_Pct| > 10%", field: "VIX_Gap_Pct", operator: ">", value: 10, test: (m) => Math.abs(getNum2(m, "VIX_Gap_Pct")) > 10, lagged: false },
9324
- { name: "Skip when |VIX_Gap_Pct| > 15%", field: "VIX_Gap_Pct", operator: ">", value: 15, test: (m) => Math.abs(getNum2(m, "VIX_Gap_Pct")) > 15, lagged: false },
9325
- // Close-derived filters (prior trading day values via LAG CTE)
9326
- // VIX (close-derived)
9327
- { name: "Skip when prior-day VIX > 25", field: "VIX_Close", operator: ">", value: 25, test: (m) => getNum2(m, "prev_VIX_Close") > 25, lagged: true },
9328
- { name: "Skip when prior-day VIX > 30", field: "VIX_Close", operator: ">", value: 30, test: (m) => getNum2(m, "prev_VIX_Close") > 30, lagged: true },
9329
- { name: "Skip when prior-day VIX < 14", field: "VIX_Close", operator: "<", value: 14, test: (m) => getNum2(m, "prev_VIX_Close") < 14, lagged: true },
9330
- // VIX spike (close-derived)
9331
- { name: "Skip when prior-day VIX_Spike > 5%", field: "VIX_Spike_Pct", operator: ">", value: 5, test: (m) => getNum2(m, "prev_VIX_Spike_Pct") > 5, lagged: true },
9332
- { name: "Skip when prior-day VIX_Spike > 8%", field: "VIX_Spike_Pct", operator: ">", value: 8, test: (m) => getNum2(m, "prev_VIX_Spike_Pct") > 8, lagged: true },
9333
- // Term structure (close-derived)
9334
- { name: "Skip prior-day backwardation", field: "Term_Structure_State", operator: "==", value: -1, test: (m) => getNum2(m, "prev_Term_Structure_State") === -1, lagged: true },
9335
- // Vol regime (close-derived)
9336
- { name: "Skip prior-day Vol Regime 5-6 (High/Extreme)", field: "Vol_Regime", operator: "in", value: [5, 6], test: (m) => getNum2(m, "prev_Vol_Regime") >= 5, lagged: true },
9337
- { name: "Skip prior-day Vol Regime 1 (Very Low)", field: "Vol_Regime", operator: "==", value: 1, test: (m) => getNum2(m, "prev_Vol_Regime") === 1, lagged: true },
9338
- // Consecutive days (close-derived)
9339
- { name: "Skip after prior-day 4+ consecutive up", field: "Consecutive_Days", operator: ">=", value: 4, test: (m) => getNum2(m, "prev_Consecutive_Days") >= 4, lagged: true },
9340
- { name: "Skip after prior-day 4+ consecutive down", field: "Consecutive_Days", operator: "<=", value: -4, test: (m) => getNum2(m, "prev_Consecutive_Days") <= -4, lagged: true },
9341
- // RSI (close-derived)
9342
- { name: "Skip when prior-day RSI > 70", field: "RSI_14", operator: ">", value: 70, test: (m) => getNum2(m, "prev_RSI_14") > 70, lagged: true },
9343
- { name: "Skip when prior-day RSI < 30", field: "RSI_14", operator: "<", value: 30, test: (m) => getNum2(m, "prev_RSI_14") < 30, lagged: true },
9344
- // Realized Vol filters (close-derived, from market.enriched)
9345
- { name: "Skip when prior-day 5D realized vol > 1.5%", field: "Realized_Vol_5D", operator: ">", value: 1.5, test: (m) => getNum2(m, "prev_Realized_Vol_5D") > 1.5, lagged: true },
9346
- { name: "Skip when prior-day 20D realized vol > 1.2%", field: "Realized_Vol_20D", operator: ">", value: 1.2, test: (m) => getNum2(m, "prev_Realized_Vol_20D") > 1.2, lagged: true },
9347
- // IVP filters (close-derived, from market.enriched ivr/ivp columns)
9348
- { name: "Skip when prior-day VIX_IVP > 80 (top 20% historically elevated vol)", field: "VIX_IVP", operator: ">", value: 80, test: (m) => getNum2(m, "prev_VIX_IVP") > 80, lagged: true },
9349
- { name: "Skip when prior-day VIX_IVP < 20 (bottom 20% historically suppressed vol)", field: "VIX_IVP", operator: "<", value: 20, test: (m) => getNum2(m, "prev_VIX_IVP") < 20, lagged: true },
9350
- // Prior_Range_vs_ATR filter (open-known, from market.enriched — same-day value)
9351
- { name: "Skip when Prior_Range_vs_ATR > 1.5 (prior day had outsized range)", field: "Prior_Range_vs_ATR", operator: ">", value: 1.5, test: (m) => getNum2(m, "Prior_Range_vs_ATR") > 1.5, lagged: false },
9352
- { name: "Skip when Prior_Range_vs_ATR < 0.5 (prior day had compressed range)", field: "Prior_Range_vs_ATR", operator: "<", value: 0.5, test: (m) => getNum2(m, "Prior_Range_vs_ATR") < 0.5, lagged: false }
9353
- ];
9354
- for (const filterDef of testFilters) {
9355
- let pool = matchedTrades;
9356
- if (filterDef.lagged) {
9357
- const prevField = `prev_${filterDef.field}`;
9358
- pool = matchedTrades.filter((t) => {
9359
- const val = getNum2(t.market, prevField);
9360
- return !isNaN(val);
9361
- });
9166
+ if (trades.length === 0) {
9167
+ return {
9168
+ content: [{ type: "text", text: "No trades found" }],
9169
+ isError: true
9170
+ };
9362
9171
  }
9363
- if (pool.length < 10) continue;
9364
- const removed = pool.filter((t) => filterDef.test(t.market));
9365
- const remaining = pool.filter((t) => !filterDef.test(t.market));
9366
- if (removed.length === 0 || remaining.length < 5) continue;
9367
- const poolWins = pool.filter((t) => t.trade.pl > 0).length;
9368
- const poolWinRate = poolWins / pool.length * 100;
9369
- const poolTotalPl = pool.reduce((sum, t) => sum + t.trade.pl, 0);
9370
- const winnersRemoved = removed.filter((t) => t.trade.pl > 0).length;
9371
- const losersRemoved = removed.length - winnersRemoved;
9372
- const newWins = remaining.filter((t) => t.trade.pl > 0).length;
9373
- const newWinRate = newWins / remaining.length * 100;
9374
- const newTotalPl = remaining.reduce((sum, t) => sum + t.trade.pl, 0);
9375
- const winRateDelta = newWinRate - poolWinRate;
9376
- const plDelta = newTotalPl - poolTotalPl;
9377
- if (winRateDelta >= minImprovementPct) {
9378
- let confidence = "low";
9379
- if (removed.length >= 10 && remaining.length >= 20) {
9380
- confidence = "high";
9381
- } else if (removed.length >= 5 && remaining.length >= 10) {
9382
- confidence = "medium";
9383
- }
9384
- suggestions.push({
9385
- filter: filterDef.name,
9386
- condition: {
9387
- field: filterDef.field,
9388
- operator: filterDef.operator,
9389
- value: filterDef.value,
9390
- lagged: filterDef.lagged
9391
- },
9392
- tradesRemoved: removed.length,
9393
- winnersRemoved,
9394
- losersRemoved,
9395
- newWinRate: Math.round(newWinRate * 100) / 100,
9396
- newTotalPl: Math.round(newTotalPl * 100) / 100,
9397
- winRateDelta: Math.round(winRateDelta * 100) / 100,
9398
- plDelta: Math.round(plDelta * 100) / 100,
9399
- confidence
9400
- });
9172
+ const tradeKeys = uniqueTradeLookupKeys2(trades);
9173
+ const conn = await getConnection(baseDir);
9174
+ const resolvedTickerSF = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
9175
+ const availabilitySF = await checkDataAvailability(stores, resolvedTickerSF);
9176
+ const { sql, params } = buildLookaheadFreeQuery(tradeKeys);
9177
+ const dailyResult = await conn.runAndReadAll(sql, params);
9178
+ const dailyRecords = resultToRecords2(dailyResult);
9179
+ const daily = recordsByTickerDate2(dailyRecords);
9180
+ const enrichedTrades = trades.map((trade) => {
9181
+ const lookup = getTradeLookupKey2(trade);
9182
+ return {
9183
+ trade,
9184
+ market: daily.get(marketTickerDateKey(lookup.ticker, lookup.date)) || null
9185
+ };
9186
+ });
9187
+ const matchedTrades = enrichedTrades.filter((t) => t.market !== null);
9188
+ if (matchedTrades.length < 10) {
9189
+ return {
9190
+ content: [
9191
+ {
9192
+ type: "text",
9193
+ text: "Not enough trades matched to market data for analysis (need at least 10)"
9194
+ }
9195
+ ],
9196
+ isError: true
9197
+ };
9401
9198
  }
9402
- }
9403
- suggestions.sort((a, b) => b.winRateDelta - a.winRateDelta);
9404
- const topSuggestions = suggestions.slice(0, 10);
9405
- const baseWinRate = currentWinRate;
9406
- const significantFilters = suggestions.filter((s) => s.winRateDelta > 3);
9407
- const compositeSuggestions = [];
9408
- const filterTestMap = /* @__PURE__ */ new Map();
9409
- for (const fd of testFilters) {
9410
- filterTestMap.set(fd.name, { test: fd.test, lagged: fd.lagged, field: fd.field });
9411
- }
9412
- for (let i = 0; i < significantFilters.length; i++) {
9413
- for (let j = i + 1; j < significantFilters.length; j++) {
9414
- const filterA = significantFilters[i];
9415
- const filterB = significantFilters[j];
9416
- const testA = filterTestMap.get(filterA.filter);
9417
- const testB = filterTestMap.get(filterB.filter);
9418
- if (!testA || !testB) continue;
9199
+ const currentWins = matchedTrades.filter((t) => t.trade.pl > 0).length;
9200
+ const currentWinRate = currentWins / matchedTrades.length * 100;
9201
+ const currentTotalPl = matchedTrades.reduce((sum, t) => sum + t.trade.pl, 0);
9202
+ const suggestions = [];
9203
+ const testFilters = [
9204
+ // Open-known filters (same-day values)
9205
+ // Gap filters
9206
+ {
9207
+ name: "Skip when |Gap_Pct| > 0.5%",
9208
+ field: "Gap_Pct",
9209
+ operator: ">",
9210
+ value: 0.5,
9211
+ test: (m) => Math.abs(getNum2(m, "Gap_Pct")) > 0.5,
9212
+ lagged: false
9213
+ },
9214
+ {
9215
+ name: "Skip when |Gap_Pct| > 0.8%",
9216
+ field: "Gap_Pct",
9217
+ operator: ">",
9218
+ value: 0.8,
9219
+ test: (m) => Math.abs(getNum2(m, "Gap_Pct")) > 0.8,
9220
+ lagged: false
9221
+ },
9222
+ {
9223
+ name: "Skip when |Gap_Pct| > 1.0%",
9224
+ field: "Gap_Pct",
9225
+ operator: ">",
9226
+ value: 1,
9227
+ test: (m) => Math.abs(getNum2(m, "Gap_Pct")) > 1,
9228
+ lagged: false
9229
+ },
9230
+ // Day of week
9231
+ {
9232
+ name: "Skip Fridays",
9233
+ field: "Day_of_Week",
9234
+ operator: "==",
9235
+ value: 6,
9236
+ test: (m) => getNum2(m, "Day_of_Week") === 6,
9237
+ lagged: false
9238
+ },
9239
+ {
9240
+ name: "Skip Mondays",
9241
+ field: "Day_of_Week",
9242
+ operator: "==",
9243
+ value: 2,
9244
+ test: (m) => getNum2(m, "Day_of_Week") === 2,
9245
+ lagged: false
9246
+ },
9247
+ // OPEX
9248
+ {
9249
+ name: "Skip OPEX days",
9250
+ field: "Is_Opex",
9251
+ operator: "==",
9252
+ value: 1,
9253
+ test: (m) => getNum2(m, "Is_Opex") === 1,
9254
+ lagged: false
9255
+ },
9256
+ // VIX_Open filters (open-known)
9257
+ {
9258
+ name: "Skip when VIX_Open > 25",
9259
+ field: "VIX_Open",
9260
+ operator: ">",
9261
+ value: 25,
9262
+ test: (m) => getNum2(m, "VIX_Open") > 25,
9263
+ lagged: false
9264
+ },
9265
+ {
9266
+ name: "Skip when VIX_Open > 30",
9267
+ field: "VIX_Open",
9268
+ operator: ">",
9269
+ value: 30,
9270
+ test: (m) => getNum2(m, "VIX_Open") > 30,
9271
+ lagged: false
9272
+ },
9273
+ // VIX_Gap_Pct filters (open-known)
9274
+ {
9275
+ name: "Skip when |VIX_Gap_Pct| > 10%",
9276
+ field: "VIX_Gap_Pct",
9277
+ operator: ">",
9278
+ value: 10,
9279
+ test: (m) => Math.abs(getNum2(m, "VIX_Gap_Pct")) > 10,
9280
+ lagged: false
9281
+ },
9282
+ {
9283
+ name: "Skip when |VIX_Gap_Pct| > 15%",
9284
+ field: "VIX_Gap_Pct",
9285
+ operator: ">",
9286
+ value: 15,
9287
+ test: (m) => Math.abs(getNum2(m, "VIX_Gap_Pct")) > 15,
9288
+ lagged: false
9289
+ },
9290
+ // Close-derived filters (prior trading day values via LAG CTE)
9291
+ // VIX (close-derived)
9292
+ {
9293
+ name: "Skip when prior-day VIX > 25",
9294
+ field: "VIX_Close",
9295
+ operator: ">",
9296
+ value: 25,
9297
+ test: (m) => getNum2(m, "prev_VIX_Close") > 25,
9298
+ lagged: true
9299
+ },
9300
+ {
9301
+ name: "Skip when prior-day VIX > 30",
9302
+ field: "VIX_Close",
9303
+ operator: ">",
9304
+ value: 30,
9305
+ test: (m) => getNum2(m, "prev_VIX_Close") > 30,
9306
+ lagged: true
9307
+ },
9308
+ {
9309
+ name: "Skip when prior-day VIX < 14",
9310
+ field: "VIX_Close",
9311
+ operator: "<",
9312
+ value: 14,
9313
+ test: (m) => getNum2(m, "prev_VIX_Close") < 14,
9314
+ lagged: true
9315
+ },
9316
+ // VIX spike (close-derived)
9317
+ {
9318
+ name: "Skip when prior-day VIX_Spike > 5%",
9319
+ field: "VIX_Spike_Pct",
9320
+ operator: ">",
9321
+ value: 5,
9322
+ test: (m) => getNum2(m, "prev_VIX_Spike_Pct") > 5,
9323
+ lagged: true
9324
+ },
9325
+ {
9326
+ name: "Skip when prior-day VIX_Spike > 8%",
9327
+ field: "VIX_Spike_Pct",
9328
+ operator: ">",
9329
+ value: 8,
9330
+ test: (m) => getNum2(m, "prev_VIX_Spike_Pct") > 8,
9331
+ lagged: true
9332
+ },
9333
+ // Term structure (close-derived)
9334
+ {
9335
+ name: "Skip prior-day backwardation",
9336
+ field: "Term_Structure_State",
9337
+ operator: "==",
9338
+ value: -1,
9339
+ test: (m) => getNum2(m, "prev_Term_Structure_State") === -1,
9340
+ lagged: true
9341
+ },
9342
+ // Vol regime (close-derived)
9343
+ {
9344
+ name: "Skip prior-day Vol Regime 5-6 (High/Extreme)",
9345
+ field: "Vol_Regime",
9346
+ operator: "in",
9347
+ value: [5, 6],
9348
+ test: (m) => getNum2(m, "prev_Vol_Regime") >= 5,
9349
+ lagged: true
9350
+ },
9351
+ {
9352
+ name: "Skip prior-day Vol Regime 1 (Very Low)",
9353
+ field: "Vol_Regime",
9354
+ operator: "==",
9355
+ value: 1,
9356
+ test: (m) => getNum2(m, "prev_Vol_Regime") === 1,
9357
+ lagged: true
9358
+ },
9359
+ // Consecutive days (close-derived)
9360
+ {
9361
+ name: "Skip after prior-day 4+ consecutive up",
9362
+ field: "Consecutive_Days",
9363
+ operator: ">=",
9364
+ value: 4,
9365
+ test: (m) => getNum2(m, "prev_Consecutive_Days") >= 4,
9366
+ lagged: true
9367
+ },
9368
+ {
9369
+ name: "Skip after prior-day 4+ consecutive down",
9370
+ field: "Consecutive_Days",
9371
+ operator: "<=",
9372
+ value: -4,
9373
+ test: (m) => getNum2(m, "prev_Consecutive_Days") <= -4,
9374
+ lagged: true
9375
+ },
9376
+ // RSI (close-derived)
9377
+ {
9378
+ name: "Skip when prior-day RSI > 70",
9379
+ field: "RSI_14",
9380
+ operator: ">",
9381
+ value: 70,
9382
+ test: (m) => getNum2(m, "prev_RSI_14") > 70,
9383
+ lagged: true
9384
+ },
9385
+ {
9386
+ name: "Skip when prior-day RSI < 30",
9387
+ field: "RSI_14",
9388
+ operator: "<",
9389
+ value: 30,
9390
+ test: (m) => getNum2(m, "prev_RSI_14") < 30,
9391
+ lagged: true
9392
+ },
9393
+ // Realized Vol filters (close-derived, from market.enriched)
9394
+ {
9395
+ name: "Skip when prior-day 5D realized vol > 1.5%",
9396
+ field: "Realized_Vol_5D",
9397
+ operator: ">",
9398
+ value: 1.5,
9399
+ test: (m) => getNum2(m, "prev_Realized_Vol_5D") > 1.5,
9400
+ lagged: true
9401
+ },
9402
+ {
9403
+ name: "Skip when prior-day 20D realized vol > 1.2%",
9404
+ field: "Realized_Vol_20D",
9405
+ operator: ">",
9406
+ value: 1.2,
9407
+ test: (m) => getNum2(m, "prev_Realized_Vol_20D") > 1.2,
9408
+ lagged: true
9409
+ },
9410
+ // IVP filters (close-derived, from market.enriched ivr/ivp columns)
9411
+ {
9412
+ name: "Skip when prior-day VIX_IVP > 80 (top 20% historically elevated vol)",
9413
+ field: "VIX_IVP",
9414
+ operator: ">",
9415
+ value: 80,
9416
+ test: (m) => getNum2(m, "prev_VIX_IVP") > 80,
9417
+ lagged: true
9418
+ },
9419
+ {
9420
+ name: "Skip when prior-day VIX_IVP < 20 (bottom 20% historically suppressed vol)",
9421
+ field: "VIX_IVP",
9422
+ operator: "<",
9423
+ value: 20,
9424
+ test: (m) => getNum2(m, "prev_VIX_IVP") < 20,
9425
+ lagged: true
9426
+ },
9427
+ // Prior_Range_vs_ATR filter (open-known, from market.enriched — same-day value)
9428
+ {
9429
+ name: "Skip when Prior_Range_vs_ATR > 1.5 (prior day had outsized range)",
9430
+ field: "Prior_Range_vs_ATR",
9431
+ operator: ">",
9432
+ value: 1.5,
9433
+ test: (m) => getNum2(m, "Prior_Range_vs_ATR") > 1.5,
9434
+ lagged: false
9435
+ },
9436
+ {
9437
+ name: "Skip when Prior_Range_vs_ATR < 0.5 (prior day had compressed range)",
9438
+ field: "Prior_Range_vs_ATR",
9439
+ operator: "<",
9440
+ value: 0.5,
9441
+ test: (m) => getNum2(m, "Prior_Range_vs_ATR") < 0.5,
9442
+ lagged: false
9443
+ }
9444
+ ];
9445
+ for (const filterDef of testFilters) {
9419
9446
  let pool = matchedTrades;
9420
- if (testA.lagged) {
9421
- const prevField = `prev_${testA.field}`;
9422
- pool = pool.filter((t) => !isNaN(getNum2(t.market, prevField)));
9447
+ if (filterDef.lagged) {
9448
+ const prevField = `prev_${filterDef.field}`;
9449
+ pool = matchedTrades.filter((t) => {
9450
+ const val = getNum2(t.market, prevField);
9451
+ return !isNaN(val);
9452
+ });
9423
9453
  }
9424
- if (testB.lagged) {
9425
- const prevField = `prev_${testB.field}`;
9426
- pool = pool.filter((t) => !isNaN(getNum2(t.market, prevField)));
9454
+ if (pool.length < 10) continue;
9455
+ const removed = pool.filter((t) => filterDef.test(t.market));
9456
+ const remaining = pool.filter(
9457
+ (t) => !filterDef.test(t.market)
9458
+ );
9459
+ if (removed.length === 0 || remaining.length < 5) continue;
9460
+ const poolWins = pool.filter((t) => t.trade.pl > 0).length;
9461
+ const poolWinRate = poolWins / pool.length * 100;
9462
+ const poolTotalPl = pool.reduce((sum, t) => sum + t.trade.pl, 0);
9463
+ const winnersRemoved = removed.filter((t) => t.trade.pl > 0).length;
9464
+ const losersRemoved = removed.length - winnersRemoved;
9465
+ const newWins = remaining.filter((t) => t.trade.pl > 0).length;
9466
+ const newWinRate = newWins / remaining.length * 100;
9467
+ const newTotalPl = remaining.reduce((sum, t) => sum + t.trade.pl, 0);
9468
+ const winRateDelta = newWinRate - poolWinRate;
9469
+ const plDelta = newTotalPl - poolTotalPl;
9470
+ if (winRateDelta >= minImprovementPct) {
9471
+ let confidence = "low";
9472
+ if (removed.length >= 10 && remaining.length >= 20) {
9473
+ confidence = "high";
9474
+ } else if (removed.length >= 5 && remaining.length >= 10) {
9475
+ confidence = "medium";
9476
+ }
9477
+ suggestions.push({
9478
+ filter: filterDef.name,
9479
+ condition: {
9480
+ field: filterDef.field,
9481
+ operator: filterDef.operator,
9482
+ value: filterDef.value,
9483
+ lagged: filterDef.lagged
9484
+ },
9485
+ tradesRemoved: removed.length,
9486
+ winnersRemoved,
9487
+ losersRemoved,
9488
+ newWinRate: Math.round(newWinRate * 100) / 100,
9489
+ newTotalPl: Math.round(newTotalPl * 100) / 100,
9490
+ winRateDelta: Math.round(winRateDelta * 100) / 100,
9491
+ plDelta: Math.round(plDelta * 100) / 100,
9492
+ confidence
9493
+ });
9427
9494
  }
9428
- const bothMatchTrades = pool.filter((t) => {
9429
- const m = t.market;
9430
- return testA.test(m) && testB.test(m);
9431
- });
9432
- if (bothMatchTrades.length < 5) continue;
9433
- const compositeWinRate = bothMatchTrades.filter((t) => t.trade.pl > 0).length / bothMatchTrades.length * 100;
9434
- const compositeAvgPl = bothMatchTrades.reduce((sum, t) => sum + t.trade.pl, 0) / bothMatchTrades.length;
9435
- const improvement = compositeWinRate - Math.max(filterA.winRateDelta + baseWinRate, filterB.winRateDelta + baseWinRate);
9436
- if (improvement > 2) {
9437
- compositeSuggestions.push({
9438
- name: `${filterA.filter} AND ${filterB.filter}`,
9439
- type: "composite",
9440
- projectedWinRate: Math.round(compositeWinRate * 100) / 100,
9441
- projectedAvgPl: Math.round(compositeAvgPl * 100) / 100,
9442
- tradesAffected: bothMatchTrades.length,
9443
- improvement: Math.round(improvement * 100) / 100
9495
+ }
9496
+ suggestions.sort((a, b) => b.winRateDelta - a.winRateDelta);
9497
+ const topSuggestions = suggestions.slice(0, 10);
9498
+ const baseWinRate = currentWinRate;
9499
+ const significantFilters = suggestions.filter((s) => s.winRateDelta > 3);
9500
+ const compositeSuggestions = [];
9501
+ const filterTestMap = /* @__PURE__ */ new Map();
9502
+ for (const fd of testFilters) {
9503
+ filterTestMap.set(fd.name, { test: fd.test, lagged: fd.lagged, field: fd.field });
9504
+ }
9505
+ for (let i = 0; i < significantFilters.length; i++) {
9506
+ for (let j = i + 1; j < significantFilters.length; j++) {
9507
+ const filterA = significantFilters[i];
9508
+ const filterB = significantFilters[j];
9509
+ const testA = filterTestMap.get(filterA.filter);
9510
+ const testB = filterTestMap.get(filterB.filter);
9511
+ if (!testA || !testB) continue;
9512
+ let pool = matchedTrades;
9513
+ if (testA.lagged) {
9514
+ const prevField = `prev_${testA.field}`;
9515
+ pool = pool.filter(
9516
+ (t) => !isNaN(getNum2(t.market, prevField))
9517
+ );
9518
+ }
9519
+ if (testB.lagged) {
9520
+ const prevField = `prev_${testB.field}`;
9521
+ pool = pool.filter(
9522
+ (t) => !isNaN(getNum2(t.market, prevField))
9523
+ );
9524
+ }
9525
+ const bothMatchTrades = pool.filter((t) => {
9526
+ const m = t.market;
9527
+ return testA.test(m) && testB.test(m);
9444
9528
  });
9529
+ if (bothMatchTrades.length < 5) continue;
9530
+ const compositeWinRate = bothMatchTrades.filter((t) => t.trade.pl > 0).length / bothMatchTrades.length * 100;
9531
+ const compositeAvgPl = bothMatchTrades.reduce((sum, t) => sum + t.trade.pl, 0) / bothMatchTrades.length;
9532
+ const improvement = compositeWinRate - Math.max(filterA.winRateDelta + baseWinRate, filterB.winRateDelta + baseWinRate);
9533
+ if (improvement > 2) {
9534
+ compositeSuggestions.push({
9535
+ name: `${filterA.filter} AND ${filterB.filter}`,
9536
+ type: "composite",
9537
+ projectedWinRate: Math.round(compositeWinRate * 100) / 100,
9538
+ projectedAvgPl: Math.round(compositeAvgPl * 100) / 100,
9539
+ tradesAffected: bothMatchTrades.length,
9540
+ improvement: Math.round(improvement * 100) / 100
9541
+ });
9542
+ }
9445
9543
  }
9446
9544
  }
9447
- }
9448
- compositeSuggestions.sort((a, b) => b.improvement - a.improvement);
9449
- const topCompositeSuggestions = compositeSuggestions.slice(0, 5);
9450
- const summary = `Filter analysis: ${blockId} | ${topSuggestions.length} standalone, ${topCompositeSuggestions.length} composite suggestions (min ${minImprovementPct}% improvement)`;
9451
- const sfResponseData = {
9452
- blockId,
9453
- lagNote: "Close-derived fields (VIX_Close, Vol_Regime, RSI_14, Consecutive_Days, VIX_Spike_Pct, Term_Structure_State, Realized_Vol_5D, Realized_Vol_20D, VIX_IVR, VIX_IVP) use prior trading day values to prevent lookahead bias. Open-known fields (Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Is_Opex) use same-day values.",
9454
- strategy: effectiveStrategy || null,
9455
- strategyName: strategyName || null,
9456
- currentStats: {
9457
- trades: matchedTrades.length,
9458
- winRate: Math.round(currentWinRate * 100) / 100,
9459
- totalPl: Math.round(currentTotalPl * 100) / 100
9460
- },
9461
- suggestedFilters: topSuggestions,
9462
- compositeSuggestions: topCompositeSuggestions,
9463
- minImprovementThreshold: minImprovementPct
9464
- };
9465
- if (availabilitySF.warnings.length > 0) {
9466
- sfResponseData.warnings = availabilitySF.warnings;
9467
- }
9468
- if (profileEntryFilters && profileEntryFilters.length > 0) {
9469
- const profileContext = [];
9470
- for (const suggestion of topSuggestions) {
9471
- const matchedFilter = profileEntryFilters.find(
9472
- (f) => f.field === suggestion.condition.field
9473
- );
9474
- profileContext.push({
9475
- suggestion: suggestion.filter,
9476
- field: suggestion.condition.field,
9477
- status: matchedFilter ? "already_in_profile" : "new_suggestion",
9478
- matchedFilter: matchedFilter ? { field: matchedFilter.field, operator: matchedFilter.operator, value: matchedFilter.value } : void 0
9479
- });
9545
+ compositeSuggestions.sort((a, b) => b.improvement - a.improvement);
9546
+ const topCompositeSuggestions = compositeSuggestions.slice(0, 5);
9547
+ const summary = `Filter analysis: ${blockId} | ${topSuggestions.length} standalone, ${topCompositeSuggestions.length} composite suggestions (min ${minImprovementPct}% improvement)`;
9548
+ const sfResponseData = {
9549
+ blockId,
9550
+ lagNote: "Close-derived fields (VIX_Close, Vol_Regime, RSI_14, Consecutive_Days, VIX_Spike_Pct, Term_Structure_State, Realized_Vol_5D, Realized_Vol_20D, VIX_IVR, VIX_IVP) use prior trading day values to prevent lookahead bias. Open-known fields (Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Is_Opex) use same-day values.",
9551
+ strategy: effectiveStrategy || null,
9552
+ strategyName: strategyName || null,
9553
+ currentStats: {
9554
+ trades: matchedTrades.length,
9555
+ winRate: Math.round(currentWinRate * 100) / 100,
9556
+ totalPl: Math.round(currentTotalPl * 100) / 100
9557
+ },
9558
+ suggestedFilters: topSuggestions,
9559
+ compositeSuggestions: topCompositeSuggestions,
9560
+ minImprovementThreshold: minImprovementPct
9561
+ };
9562
+ if (availabilitySF.warnings.length > 0) {
9563
+ sfResponseData.warnings = availabilitySF.warnings;
9480
9564
  }
9481
- sfResponseData.profile_context = {
9482
- strategyName,
9483
- existingFilters: profileEntryFilters.length,
9484
- crossReference: profileContext
9565
+ if (profileEntryFilters && profileEntryFilters.length > 0) {
9566
+ const profileContext = [];
9567
+ for (const suggestion of topSuggestions) {
9568
+ const matchedFilter = profileEntryFilters.find(
9569
+ (f) => f.field === suggestion.condition.field
9570
+ );
9571
+ profileContext.push({
9572
+ suggestion: suggestion.filter,
9573
+ field: suggestion.condition.field,
9574
+ status: matchedFilter ? "already_in_profile" : "new_suggestion",
9575
+ matchedFilter: matchedFilter ? {
9576
+ field: matchedFilter.field,
9577
+ operator: matchedFilter.operator,
9578
+ value: matchedFilter.value
9579
+ } : void 0
9580
+ });
9581
+ }
9582
+ sfResponseData.profile_context = {
9583
+ strategyName,
9584
+ existingFilters: profileEntryFilters.length,
9585
+ crossReference: profileContext
9586
+ };
9587
+ }
9588
+ return createToolOutput(summary, sfResponseData);
9589
+ } catch (error) {
9590
+ return {
9591
+ content: [{ type: "text", text: `Error: ${error.message}` }],
9592
+ isError: true
9485
9593
  };
9486
9594
  }
9487
- return createToolOutput(summary, sfResponseData);
9488
- } catch (error) {
9489
- return {
9490
- content: [{ type: "text", text: `Error: ${error.message}` }],
9491
- isError: true
9492
- };
9493
9595
  }
9494
- })
9596
+ )
9495
9597
  );
9496
9598
  server.registerTool(
9497
9599
  "enrich_trades",
@@ -9511,166 +9613,182 @@ function registerMarketDataTools(server, baseDir, stores) {
9511
9613
  offset: z14.number().min(0).default(0).describe("Pagination offset (default: 0)")
9512
9614
  })
9513
9615
  },
9514
- withFullSync(baseDir, async ({ blockId, strategy, startDate, endDate, ticker, includeOutcomeFields, includeIntradayContext, limit, offset }) => {
9515
- try {
9516
- const block = await loadBlock(baseDir, blockId);
9517
- let trades = block.trades;
9518
- trades = filterByStrategy(trades, strategy);
9519
- trades = filterByDateRange(trades, startDate, endDate);
9520
- if (trades.length === 0) {
9521
- return {
9522
- content: [{ type: "text", text: "No trades found matching filters" }],
9523
- isError: true
9524
- };
9525
- }
9526
- const totalTrades = trades.length;
9527
- const paginated = trades.slice(offset, offset + limit);
9528
- if (paginated.length === 0) {
9529
- return createToolOutput(
9530
- `Enriched trades: ${blockId} | 0/0 matched | offset ${offset}, limit ${limit}`,
9531
- {
9532
- blockId,
9533
- strategy: strategy || null,
9534
- lagNote: "",
9535
- tradesTotal: totalTrades,
9536
- returned: 0,
9537
- offset,
9538
- hasMore: false,
9539
- tradesMatched: 0,
9540
- unmatchedDates: [],
9541
- trades: []
9542
- }
9543
- );
9544
- }
9545
- const tradeKeys = uniqueTradeLookupKeys2(paginated);
9546
- const conn = await getConnection(baseDir);
9547
- const resolvedTickerET = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
9548
- const availabilityET = await checkDataAvailability(stores, resolvedTickerET, { checkIntraday: includeIntradayContext });
9549
- const { sql: lagSql, params: lagParams } = buildLookaheadFreeQuery(tradeKeys);
9550
- const dailyResult = await conn.runAndReadAll(lagSql, lagParams);
9551
- const dailyRecords = resultToRecords2(dailyResult);
9552
- const daily = recordsByTickerDate2(dailyRecords);
9553
- let outcomeMap = null;
9554
- if (includeOutcomeFields) {
9555
- const { sql: outcomeSql, params: outcomeParams } = buildOutcomeQuery(tradeKeys);
9556
- const outcomeResult = await conn.runAndReadAll(outcomeSql, outcomeParams);
9557
- const outcomeRecords = resultToRecords2(outcomeResult);
9558
- outcomeMap = recordsByTickerDate2(outcomeRecords);
9559
- }
9560
- let intradayBarsByKey = null;
9561
- if (includeIntradayContext) {
9562
- intradayBarsByKey = /* @__PURE__ */ new Map();
9563
- for (const key of tradeKeys) {
9564
- const bars = await stores.spot.readBars(key.ticker, key.date, key.date);
9565
- if (bars.length === 0) continue;
9566
- const mapKey = marketTickerDateKey(key.ticker, key.date);
9567
- const list = intradayBarsByKey.get(mapKey) ?? [];
9568
- for (const bar of bars) {
9569
- if (!Number.isFinite(bar.open) || bar.open <= 0 || !Number.isFinite(bar.high) || bar.high <= 0 || !Number.isFinite(bar.low) || bar.low <= 0 || !Number.isFinite(bar.close) || bar.close <= 0) continue;
9570
- list.push({
9571
- time: String(bar.time),
9572
- open: Number(bar.open),
9573
- high: Number(bar.high),
9574
- low: Number(bar.low),
9575
- close: Number(bar.close)
9576
- });
9577
- }
9578
- intradayBarsByKey.set(mapKey, list);
9579
- }
9580
- }
9581
- const cleanVal = (val) => {
9582
- if (typeof val === "bigint") return Number(val);
9583
- if (typeof val === "number" && isNaN(val)) return null;
9584
- return val === void 0 ? null : val;
9585
- };
9586
- const unmatchedDates = [];
9587
- let matched = 0;
9588
- const enrichedTrades = paginated.map((trade) => {
9589
- const lookup = getTradeLookupKey2(trade);
9590
- const marketKey = marketTickerDateKey(lookup.ticker, lookup.date);
9591
- const marketData = daily.get(marketKey);
9592
- const commissions = trade.openingCommissionsFees + trade.closingCommissionsFees;
9593
- const baseTrade = {
9594
- dateOpened: lookup.date,
9595
- ticker: lookup.ticker,
9596
- timeOpened: trade.timeOpened,
9597
- strategy: trade.strategy,
9598
- legs: trade.legs,
9599
- pl: trade.pl,
9600
- numContracts: trade.numContracts,
9601
- premium: trade.premium,
9602
- reasonForClose: trade.reasonForClose || null,
9603
- commissions
9604
- };
9605
- if (!marketData) {
9606
- unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
9607
- baseTrade.entryContext = null;
9608
- return baseTrade;
9616
+ withFullSync(
9617
+ baseDir,
9618
+ async ({
9619
+ blockId,
9620
+ strategy,
9621
+ startDate,
9622
+ endDate,
9623
+ ticker,
9624
+ includeOutcomeFields,
9625
+ includeIntradayContext,
9626
+ limit,
9627
+ offset
9628
+ }) => {
9629
+ try {
9630
+ const block = await loadBlock(baseDir, blockId);
9631
+ let trades = block.trades;
9632
+ trades = filterByStrategy(trades, strategy);
9633
+ trades = filterByDateRange(trades, startDate, endDate);
9634
+ if (trades.length === 0) {
9635
+ return {
9636
+ content: [{ type: "text", text: "No trades found matching filters" }],
9637
+ isError: true
9638
+ };
9609
9639
  }
9610
- matched++;
9611
- const sameDay = {};
9612
- for (const field of OPEN_KNOWN_FIELDS) {
9613
- sameDay[field] = cleanVal(marketData[field]);
9640
+ const totalTrades = trades.length;
9641
+ const paginated = trades.slice(offset, offset + limit);
9642
+ if (paginated.length === 0) {
9643
+ return createToolOutput(
9644
+ `Enriched trades: ${blockId} | 0/0 matched | offset ${offset}, limit ${limit}`,
9645
+ {
9646
+ blockId,
9647
+ strategy: strategy || null,
9648
+ lagNote: "",
9649
+ tradesTotal: totalTrades,
9650
+ returned: 0,
9651
+ offset,
9652
+ hasMore: false,
9653
+ tradesMatched: 0,
9654
+ unmatchedDates: [],
9655
+ trades: []
9656
+ }
9657
+ );
9614
9658
  }
9615
- for (const field of STATIC_FIELDS) {
9616
- sameDay[field] = cleanVal(marketData[field]);
9659
+ const tradeKeys = uniqueTradeLookupKeys2(paginated);
9660
+ const conn = await getConnection(baseDir);
9661
+ const resolvedTickerET = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
9662
+ const availabilityET = await checkDataAvailability(stores, resolvedTickerET, {
9663
+ checkIntraday: includeIntradayContext
9664
+ });
9665
+ const { sql: lagSql, params: lagParams } = buildLookaheadFreeQuery(tradeKeys);
9666
+ const dailyResult = await conn.runAndReadAll(lagSql, lagParams);
9667
+ const dailyRecords = resultToRecords2(dailyResult);
9668
+ const daily = recordsByTickerDate2(dailyRecords);
9669
+ let outcomeMap = null;
9670
+ if (includeOutcomeFields) {
9671
+ const { sql: outcomeSql, params: outcomeParams } = buildOutcomeQuery(tradeKeys);
9672
+ const outcomeResult = await conn.runAndReadAll(outcomeSql, outcomeParams);
9673
+ const outcomeRecords = resultToRecords2(outcomeResult);
9674
+ outcomeMap = recordsByTickerDate2(outcomeRecords);
9617
9675
  }
9618
- const priorDay = {};
9619
- for (const field of CLOSE_KNOWN_FIELDS) {
9620
- priorDay[field] = cleanVal(marketData[`prev_${field}`]);
9676
+ let intradayBarsByKey = null;
9677
+ if (includeIntradayContext) {
9678
+ intradayBarsByKey = /* @__PURE__ */ new Map();
9679
+ for (const key of tradeKeys) {
9680
+ const bars = await stores.spot.readBars(key.ticker, key.date, key.date);
9681
+ if (bars.length === 0) continue;
9682
+ const mapKey = marketTickerDateKey(key.ticker, key.date);
9683
+ const list = intradayBarsByKey.get(mapKey) ?? [];
9684
+ for (const bar of bars) {
9685
+ if (!Number.isFinite(bar.open) || bar.open <= 0 || !Number.isFinite(bar.high) || bar.high <= 0 || !Number.isFinite(bar.low) || bar.low <= 0 || !Number.isFinite(bar.close) || bar.close <= 0)
9686
+ continue;
9687
+ list.push({
9688
+ time: String(bar.time),
9689
+ open: Number(bar.open),
9690
+ high: Number(bar.high),
9691
+ low: Number(bar.low),
9692
+ close: Number(bar.close)
9693
+ });
9694
+ }
9695
+ intradayBarsByKey.set(mapKey, list);
9696
+ }
9621
9697
  }
9622
- baseTrade.entryContext = { sameDay, priorDay };
9623
- if (includeOutcomeFields && outcomeMap) {
9624
- const outcomeData = outcomeMap.get(marketKey);
9625
- if (outcomeData) {
9626
- const outcomeFields = {};
9627
- for (const field of CLOSE_KNOWN_FIELDS) {
9628
- outcomeFields[field] = cleanVal(outcomeData[field]);
9698
+ const cleanVal = (val) => {
9699
+ if (typeof val === "bigint") return Number(val);
9700
+ if (typeof val === "number" && isNaN(val)) return null;
9701
+ return val === void 0 ? null : val;
9702
+ };
9703
+ const unmatchedDates = [];
9704
+ let matched = 0;
9705
+ const enrichedTrades = paginated.map((trade) => {
9706
+ const lookup = getTradeLookupKey2(trade);
9707
+ const marketKey = marketTickerDateKey(lookup.ticker, lookup.date);
9708
+ const marketData = daily.get(marketKey);
9709
+ const commissions = trade.openingCommissionsFees + trade.closingCommissionsFees;
9710
+ const baseTrade = {
9711
+ dateOpened: lookup.date,
9712
+ ticker: lookup.ticker,
9713
+ timeOpened: trade.timeOpened,
9714
+ strategy: trade.strategy,
9715
+ legs: trade.legs,
9716
+ pl: trade.pl,
9717
+ numContracts: trade.numContracts,
9718
+ premium: trade.premium,
9719
+ reasonForClose: trade.reasonForClose || null,
9720
+ commissions
9721
+ };
9722
+ if (!marketData) {
9723
+ unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
9724
+ baseTrade.entryContext = null;
9725
+ return baseTrade;
9726
+ }
9727
+ matched++;
9728
+ const sameDay = {};
9729
+ for (const field of OPEN_KNOWN_FIELDS) {
9730
+ sameDay[field] = cleanVal(marketData[field]);
9731
+ }
9732
+ for (const field of STATIC_FIELDS) {
9733
+ sameDay[field] = cleanVal(marketData[field]);
9734
+ }
9735
+ const priorDay = {};
9736
+ for (const field of CLOSE_KNOWN_FIELDS) {
9737
+ priorDay[field] = cleanVal(marketData[`prev_${field}`]);
9738
+ }
9739
+ baseTrade.entryContext = { sameDay, priorDay };
9740
+ if (includeOutcomeFields && outcomeMap) {
9741
+ const outcomeData = outcomeMap.get(marketKey);
9742
+ if (outcomeData) {
9743
+ const outcomeFields = {};
9744
+ for (const field of CLOSE_KNOWN_FIELDS) {
9745
+ outcomeFields[field] = cleanVal(outcomeData[field]);
9746
+ }
9747
+ baseTrade.outcomeFields = outcomeFields;
9629
9748
  }
9630
- baseTrade.outcomeFields = outcomeFields;
9631
9749
  }
9750
+ if (includeIntradayContext && intradayBarsByKey) {
9751
+ const bars = intradayBarsByKey.get(marketKey) || null;
9752
+ baseTrade.intradayBars = bars;
9753
+ }
9754
+ return baseTrade;
9755
+ });
9756
+ const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
9757
+ let lagNote = "Entry context uses lookahead-free temporal joins on ticker+date via market.enriched + market.spot_daily LEFT JOIN VIX tickers + market.enriched_context. Same-day (open-known) fields: Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Month, Is_Opex. Prior-day (close-derived) fields: VIX_Close, RSI_14, Vol_Regime, Realized_Vol_5D, Realized_Vol_20D, VIX_IVR, VIX_IVP, Term_Structure_State, etc. \u2014 use the previous trading day's close-derived values to prevent lookahead bias.";
9758
+ if (includeOutcomeFields) {
9759
+ lagNote += " Outcome fields contain same-day close-derived values and represent information NOT available at trade entry time.";
9632
9760
  }
9633
- if (includeIntradayContext && intradayBarsByKey) {
9634
- const bars = intradayBarsByKey.get(marketKey) || null;
9635
- baseTrade.intradayBars = bars;
9761
+ if (includeIntradayContext) {
9762
+ lagNote += " intradayBars contains raw OHLC bar arrays from market.spot keyed by ticker+date.";
9636
9763
  }
9637
- return baseTrade;
9638
- });
9639
- const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
9640
- let lagNote = "Entry context uses lookahead-free temporal joins on ticker+date via market.enriched + market.spot_daily LEFT JOIN VIX tickers + market.enriched_context. Same-day (open-known) fields: Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Month, Is_Opex. Prior-day (close-derived) fields: VIX_Close, RSI_14, Vol_Regime, Realized_Vol_5D, Realized_Vol_20D, VIX_IVR, VIX_IVP, Term_Structure_State, etc. \u2014 use the previous trading day's close-derived values to prevent lookahead bias.";
9641
- if (includeOutcomeFields) {
9642
- lagNote += " Outcome fields contain same-day close-derived values and represent information NOT available at trade entry time.";
9643
- }
9644
- if (includeIntradayContext) {
9645
- lagNote += " intradayBars contains raw OHLC bar arrays from market.spot keyed by ticker+date.";
9646
- }
9647
- const responseData = {
9648
- blockId,
9649
- strategy: strategy || null,
9650
- lagNote,
9651
- tradesTotal: totalTrades,
9652
- returned: paginated.length,
9653
- offset,
9654
- hasMore: offset + limit < totalTrades,
9655
- tradesMatched: matched,
9656
- unmatchedDates: sortedUnmatchedDates,
9657
- trades: enrichedTrades
9658
- };
9659
- if (includeOutcomeFields) {
9660
- responseData.lookaheadWarning = "WARNING: outcomeFields contain same-day close-derived values that were NOT available at trade entry time. Do not use these for entry signal analysis.";
9661
- }
9662
- if (availabilityET.warnings.length > 0) {
9663
- responseData.warnings = availabilityET.warnings;
9764
+ const responseData = {
9765
+ blockId,
9766
+ strategy: strategy || null,
9767
+ lagNote,
9768
+ tradesTotal: totalTrades,
9769
+ returned: paginated.length,
9770
+ offset,
9771
+ hasMore: offset + limit < totalTrades,
9772
+ tradesMatched: matched,
9773
+ unmatchedDates: sortedUnmatchedDates,
9774
+ trades: enrichedTrades
9775
+ };
9776
+ if (includeOutcomeFields) {
9777
+ responseData.lookaheadWarning = "WARNING: outcomeFields contain same-day close-derived values that were NOT available at trade entry time. Do not use these for entry signal analysis.";
9778
+ }
9779
+ if (availabilityET.warnings.length > 0) {
9780
+ responseData.warnings = availabilityET.warnings;
9781
+ }
9782
+ const summary = `Enriched trades: ${blockId} | ${matched}/${paginated.length} matched | offset ${offset}, limit ${limit}`;
9783
+ return createToolOutput(summary, responseData);
9784
+ } catch (error) {
9785
+ return {
9786
+ content: [{ type: "text", text: `Error: ${error.message}` }],
9787
+ isError: true
9788
+ };
9664
9789
  }
9665
- const summary = `Enriched trades: ${blockId} | ${matched}/${paginated.length} matched | offset ${offset}, limit ${limit}`;
9666
- return createToolOutput(summary, responseData);
9667
- } catch (error) {
9668
- return {
9669
- content: [{ type: "text", text: `Error: ${error.message}` }],
9670
- isError: true
9671
- };
9672
9790
  }
9673
- })
9791
+ )
9674
9792
  );
9675
9793
  function hhmmToSqlTime(hhmm) {
9676
9794
  if (!/^\d{4}$/.test(hhmm)) {
@@ -9693,169 +9811,181 @@ function registerMarketDataTools(server, baseDir, stores) {
9693
9811
  limit: z14.number().min(1).max(500).default(100).describe("Max days to return")
9694
9812
  })
9695
9813
  },
9696
- withFullSync(baseDir, async ({ ticker, startDate, endDate, startTime, endTime, useHighLow, barResolution, limit }) => {
9697
- try {
9698
- const normalizedTicker = normalizeTicker(ticker) || "SPX";
9699
- const end = endDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9700
- const sqlStartTime = hhmmToSqlTime(startTime);
9701
- const sqlEndTime = hhmmToSqlTime(endTime);
9702
- const availability = await checkDataAvailability(stores, normalizedTicker, { checkIntraday: true });
9703
- const wideCoverage = await stores.spot.getCoverage(
9704
- normalizedTicker,
9705
- "2000-01-01",
9706
- (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
9707
- );
9708
- if (wideCoverage.totalDates === 0) {
9709
- return createToolOutput(
9710
- `ORB (${normalizedTicker}): No intraday data available`,
9711
- {
9814
+ withFullSync(
9815
+ baseDir,
9816
+ async ({
9817
+ ticker,
9818
+ startDate,
9819
+ endDate,
9820
+ startTime,
9821
+ endTime,
9822
+ useHighLow,
9823
+ barResolution,
9824
+ limit
9825
+ }) => {
9826
+ try {
9827
+ const normalizedTicker = normalizeTicker(ticker) || "SPX";
9828
+ const end = endDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9829
+ const sqlStartTime = hhmmToSqlTime(startTime);
9830
+ const sqlEndTime = hhmmToSqlTime(endTime);
9831
+ const availability = await checkDataAvailability(stores, normalizedTicker, {
9832
+ checkIntraday: true
9833
+ });
9834
+ const wideCoverage = await stores.spot.getCoverage(
9835
+ normalizedTicker,
9836
+ "2000-01-01",
9837
+ (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
9838
+ );
9839
+ if (wideCoverage.totalDates === 0) {
9840
+ return createToolOutput(`ORB (${normalizedTicker}): No intraday data available`, {
9712
9841
  query: { ticker: normalizedTicker, startTime, endTime, startDate, endDate: end },
9713
9842
  warnings: availability.warnings,
9714
9843
  days: [],
9715
9844
  stats: { totalDays: 0 }
9845
+ });
9846
+ }
9847
+ let resolvedBarResolution = null;
9848
+ if (barResolution !== void 0 && barResolution !== null) {
9849
+ const parsed = parseInt(barResolution, 10);
9850
+ if (!isNaN(parsed) && parsed > 0) {
9851
+ resolvedBarResolution = parsed;
9852
+ }
9853
+ } else if (wideCoverage.earliest) {
9854
+ try {
9855
+ const sampleBars = await stores.spot.readBars(
9856
+ normalizedTicker,
9857
+ wideCoverage.earliest,
9858
+ wideCoverage.earliest
9859
+ );
9860
+ const distinctTimes = [];
9861
+ for (const bar of sampleBars) {
9862
+ const t = String(bar.time);
9863
+ if (distinctTimes.length === 0 || distinctTimes[distinctTimes.length - 1] !== t) {
9864
+ distinctTimes.push(t);
9865
+ if (distinctTimes.length === 10) break;
9866
+ }
9867
+ }
9868
+ if (distinctTimes.length >= 2) {
9869
+ const t1 = distinctTimes[0];
9870
+ const t2 = distinctTimes[1];
9871
+ const [h1, m1] = t1.split(":").map(Number);
9872
+ const [h2, m2] = t2.split(":").map(Number);
9873
+ const gap = h2 * 60 + m2 - (h1 * 60 + m1);
9874
+ if (gap > 0) {
9875
+ resolvedBarResolution = gap;
9876
+ }
9877
+ }
9878
+ } catch {
9716
9879
  }
9717
- );
9718
- }
9719
- let resolvedBarResolution = null;
9720
- if (barResolution !== void 0 && barResolution !== null) {
9721
- const parsed = parseInt(barResolution, 10);
9722
- if (!isNaN(parsed) && parsed > 0) {
9723
- resolvedBarResolution = parsed;
9724
9880
  }
9725
- } else if (wideCoverage.earliest) {
9726
- try {
9727
- const sampleBars = await stores.spot.readBars(
9728
- normalizedTicker,
9729
- wideCoverage.earliest,
9730
- wideCoverage.earliest
9881
+ const allBars = await stores.spot.readBars(normalizedTicker, startDate, end);
9882
+ const barsByDate = /* @__PURE__ */ new Map();
9883
+ for (const bar of allBars) {
9884
+ if (!Number.isFinite(bar.open) || bar.open <= 0 || !Number.isFinite(bar.high) || bar.high <= 0 || !Number.isFinite(bar.low) || bar.low <= 0 || !Number.isFinite(bar.close) || bar.close <= 0)
9885
+ continue;
9886
+ const arr = barsByDate.get(bar.date);
9887
+ if (arr) arr.push(bar);
9888
+ else barsByDate.set(bar.date, [bar]);
9889
+ }
9890
+ const days = [];
9891
+ const sortedDates = [...barsByDate.keys()].sort();
9892
+ for (const date of sortedDates) {
9893
+ const dayBars = barsByDate.get(date);
9894
+ const windowBars = dayBars.filter(
9895
+ (b) => String(b.time) >= sqlStartTime && String(b.time) <= sqlEndTime
9731
9896
  );
9732
- const distinctTimes = [];
9733
- for (const bar of sampleBars) {
9734
- const t = String(bar.time);
9735
- if (distinctTimes.length === 0 || distinctTimes[distinctTimes.length - 1] !== t) {
9736
- distinctTimes.push(t);
9737
- if (distinctTimes.length === 10) break;
9897
+ if (windowBars.length === 0) continue;
9898
+ let orbHigh = -Infinity;
9899
+ let orbLow = Infinity;
9900
+ let orbOpen = null;
9901
+ for (const b of windowBars) {
9902
+ const hi = useHighLow ? Number(b.high) : Number(b.close);
9903
+ const lo = useHighLow ? Number(b.low) : Number(b.close);
9904
+ if (hi > orbHigh) orbHigh = hi;
9905
+ if (lo < orbLow) orbLow = lo;
9906
+ if (String(b.time) === sqlStartTime && orbOpen === null) {
9907
+ orbOpen = Number(b.open);
9738
9908
  }
9739
9909
  }
9740
- if (distinctTimes.length >= 2) {
9741
- const t1 = distinctTimes[0];
9742
- const t2 = distinctTimes[1];
9743
- const [h1, m1] = t1.split(":").map(Number);
9744
- const [h2, m2] = t2.split(":").map(Number);
9745
- const gap = h2 * 60 + m2 - (h1 * 60 + m1);
9746
- if (gap > 0) {
9747
- resolvedBarResolution = gap;
9748
- }
9910
+ const orbRange = orbHigh - orbLow;
9911
+ const orbRangePct = orbLow > 0 ? orbRange / orbLow * 100 : 0;
9912
+ let breakoutUpTime = null;
9913
+ let breakoutDownTime = null;
9914
+ for (const b of dayBars) {
9915
+ const t = String(b.time);
9916
+ if (t <= sqlEndTime) continue;
9917
+ const upHit = useHighLow ? Number(b.high) > orbHigh : Number(b.close) > orbHigh;
9918
+ const downHit = useHighLow ? Number(b.low) < orbLow : Number(b.close) < orbLow;
9919
+ if (upHit && breakoutUpTime === null) breakoutUpTime = t;
9920
+ if (downHit && breakoutDownTime === null) breakoutDownTime = t;
9921
+ if (breakoutUpTime !== null && breakoutDownTime !== null) break;
9749
9922
  }
9750
- } catch {
9751
- }
9752
- }
9753
- const allBars = await stores.spot.readBars(normalizedTicker, startDate, end);
9754
- const barsByDate = /* @__PURE__ */ new Map();
9755
- for (const bar of allBars) {
9756
- if (!Number.isFinite(bar.open) || bar.open <= 0 || !Number.isFinite(bar.high) || bar.high <= 0 || !Number.isFinite(bar.low) || bar.low <= 0 || !Number.isFinite(bar.close) || bar.close <= 0) continue;
9757
- const arr = barsByDate.get(bar.date);
9758
- if (arr) arr.push(bar);
9759
- else barsByDate.set(bar.date, [bar]);
9760
- }
9761
- const days = [];
9762
- const sortedDates = [...barsByDate.keys()].sort();
9763
- for (const date of sortedDates) {
9764
- const dayBars = barsByDate.get(date);
9765
- const windowBars = dayBars.filter(
9766
- (b) => String(b.time) >= sqlStartTime && String(b.time) <= sqlEndTime
9767
- );
9768
- if (windowBars.length === 0) continue;
9769
- let orbHigh = -Infinity;
9770
- let orbLow = Infinity;
9771
- let orbOpen = null;
9772
- for (const b of windowBars) {
9773
- const hi = useHighLow ? Number(b.high) : Number(b.close);
9774
- const lo = useHighLow ? Number(b.low) : Number(b.close);
9775
- if (hi > orbHigh) orbHigh = hi;
9776
- if (lo < orbLow) orbLow = lo;
9777
- if (String(b.time) === sqlStartTime && orbOpen === null) {
9778
- orbOpen = Number(b.open);
9923
+ let breakoutCondition;
9924
+ if (breakoutUpTime !== null && breakoutDownTime !== null) {
9925
+ breakoutCondition = breakoutUpTime < breakoutDownTime ? "HighFirst" : "LowFirst";
9926
+ } else if (breakoutUpTime !== null) {
9927
+ breakoutCondition = "HighOnly";
9928
+ } else if (breakoutDownTime !== null) {
9929
+ breakoutCondition = "LowOnly";
9930
+ } else {
9931
+ breakoutCondition = "NoBreakout";
9779
9932
  }
9933
+ days.push({
9934
+ date,
9935
+ ORB_High: Math.round(orbHigh * 100) / 100,
9936
+ ORB_Low: Math.round(orbLow * 100) / 100,
9937
+ ORB_Range: Math.round(orbRange * 100) / 100,
9938
+ ORB_Range_Pct: Math.round(orbRangePct * 1e4) / 1e4,
9939
+ ORB_Open: orbOpen !== null ? Math.round(orbOpen * 100) / 100 : null,
9940
+ breakout_condition: breakoutCondition,
9941
+ breakout_up_time: breakoutUpTime,
9942
+ breakout_down_time: breakoutDownTime,
9943
+ entry_triggered: breakoutCondition !== "NoBreakout"
9944
+ });
9780
9945
  }
9781
- const orbRange = orbHigh - orbLow;
9782
- const orbRangePct = orbLow > 0 ? orbRange / orbLow * 100 : 0;
9783
- let breakoutUpTime = null;
9784
- let breakoutDownTime = null;
9785
- for (const b of dayBars) {
9786
- const t = String(b.time);
9787
- if (t <= sqlEndTime) continue;
9788
- const upHit = useHighLow ? Number(b.high) > orbHigh : Number(b.close) > orbHigh;
9789
- const downHit = useHighLow ? Number(b.low) < orbLow : Number(b.close) < orbLow;
9790
- if (upHit && breakoutUpTime === null) breakoutUpTime = t;
9791
- if (downHit && breakoutDownTime === null) breakoutDownTime = t;
9792
- if (breakoutUpTime !== null && breakoutDownTime !== null) break;
9793
- }
9794
- let breakoutCondition;
9795
- if (breakoutUpTime !== null && breakoutDownTime !== null) {
9796
- breakoutCondition = breakoutUpTime < breakoutDownTime ? "HighFirst" : "LowFirst";
9797
- } else if (breakoutUpTime !== null) {
9798
- breakoutCondition = "HighOnly";
9799
- } else if (breakoutDownTime !== null) {
9800
- breakoutCondition = "LowOnly";
9801
- } else {
9802
- breakoutCondition = "NoBreakout";
9946
+ const totalDays = days.length;
9947
+ const avgOrbRangePct = totalDays > 0 ? days.reduce((sum, d) => sum + d.ORB_Range_Pct, 0) / totalDays : 0;
9948
+ const breakdownByCondition = {
9949
+ HighFirst: days.filter((d) => d.breakout_condition === "HighFirst").length,
9950
+ LowFirst: days.filter((d) => d.breakout_condition === "LowFirst").length,
9951
+ HighOnly: days.filter((d) => d.breakout_condition === "HighOnly").length,
9952
+ LowOnly: days.filter((d) => d.breakout_condition === "LowOnly").length,
9953
+ NoBreakout: days.filter((d) => d.breakout_condition === "NoBreakout").length
9954
+ };
9955
+ const limitedDays = days.slice(0, limit);
9956
+ const summary = `ORB (${normalizedTicker}, ${startTime}-${endTime}): ${startDate} to ${end} | ${totalDays} days, avg range ${formatPercent(avgOrbRangePct)}`;
9957
+ const responseData = {
9958
+ query: {
9959
+ ticker: normalizedTicker,
9960
+ startTime,
9961
+ endTime,
9962
+ sqlStartTime,
9963
+ sqlEndTime,
9964
+ startDate,
9965
+ endDate: end,
9966
+ useHighLow,
9967
+ barResolution: resolvedBarResolution !== null ? String(resolvedBarResolution) : "auto"
9968
+ },
9969
+ stats: {
9970
+ totalDays,
9971
+ avgOrbRangePct: Math.round(avgOrbRangePct * 1e4) / 1e4,
9972
+ breakdownByCondition
9973
+ },
9974
+ returned: limitedDays.length,
9975
+ days: limitedDays
9976
+ };
9977
+ if (availability.warnings.length > 0) {
9978
+ responseData.warnings = availability.warnings;
9803
9979
  }
9804
- days.push({
9805
- date,
9806
- ORB_High: Math.round(orbHigh * 100) / 100,
9807
- ORB_Low: Math.round(orbLow * 100) / 100,
9808
- ORB_Range: Math.round(orbRange * 100) / 100,
9809
- ORB_Range_Pct: Math.round(orbRangePct * 1e4) / 1e4,
9810
- ORB_Open: orbOpen !== null ? Math.round(orbOpen * 100) / 100 : null,
9811
- breakout_condition: breakoutCondition,
9812
- breakout_up_time: breakoutUpTime,
9813
- breakout_down_time: breakoutDownTime,
9814
- entry_triggered: breakoutCondition !== "NoBreakout"
9815
- });
9816
- }
9817
- const totalDays = days.length;
9818
- const avgOrbRangePct = totalDays > 0 ? days.reduce((sum, d) => sum + d.ORB_Range_Pct, 0) / totalDays : 0;
9819
- const breakdownByCondition = {
9820
- HighFirst: days.filter((d) => d.breakout_condition === "HighFirst").length,
9821
- LowFirst: days.filter((d) => d.breakout_condition === "LowFirst").length,
9822
- HighOnly: days.filter((d) => d.breakout_condition === "HighOnly").length,
9823
- LowOnly: days.filter((d) => d.breakout_condition === "LowOnly").length,
9824
- NoBreakout: days.filter((d) => d.breakout_condition === "NoBreakout").length
9825
- };
9826
- const limitedDays = days.slice(0, limit);
9827
- const summary = `ORB (${normalizedTicker}, ${startTime}-${endTime}): ${startDate} to ${end} | ${totalDays} days, avg range ${formatPercent(avgOrbRangePct)}`;
9828
- const responseData = {
9829
- query: {
9830
- ticker: normalizedTicker,
9831
- startTime,
9832
- endTime,
9833
- sqlStartTime,
9834
- sqlEndTime,
9835
- startDate,
9836
- endDate: end,
9837
- useHighLow,
9838
- barResolution: resolvedBarResolution !== null ? String(resolvedBarResolution) : "auto"
9839
- },
9840
- stats: {
9841
- totalDays,
9842
- avgOrbRangePct: Math.round(avgOrbRangePct * 1e4) / 1e4,
9843
- breakdownByCondition
9844
- },
9845
- returned: limitedDays.length,
9846
- days: limitedDays
9847
- };
9848
- if (availability.warnings.length > 0) {
9849
- responseData.warnings = availability.warnings;
9980
+ return createToolOutput(summary, responseData);
9981
+ } catch (error) {
9982
+ return {
9983
+ content: [{ type: "text", text: `Error: ${error.message}` }],
9984
+ isError: true
9985
+ };
9850
9986
  }
9851
- return createToolOutput(summary, responseData);
9852
- } catch (error) {
9853
- return {
9854
- content: [{ type: "text", text: `Error: ${error.message}` }],
9855
- isError: true
9856
- };
9857
9987
  }
9858
- })
9988
+ )
9859
9989
  );
9860
9990
  }
9861
9991
 
@@ -9945,7 +10075,9 @@ function applyColumnMapping(rows, columnMapping, ticker) {
9945
10075
  if (hasNullDate) continue;
9946
10076
  if (!("date" in mapped)) continue;
9947
10077
  if (!("time" in mapped)) {
9948
- const dateSourceCol = Object.entries(columnMapping).find(([, schema]) => schema === "date")?.[0];
10078
+ const dateSourceCol = Object.entries(columnMapping).find(
10079
+ ([, schema]) => schema === "date"
10080
+ )?.[0];
9949
10081
  if (dateSourceCol) {
9950
10082
  const rawDateValue = row[dateSourceCol] ?? "";
9951
10083
  const numericDate = Number(rawDateValue);
@@ -10436,9 +10568,7 @@ async function executeWithTimeout(conn, sql, limit, timeoutMs = DEFAULT_TIMEOUT_
10436
10568
  const timeoutPromise = new Promise((_, reject) => {
10437
10569
  setTimeout(() => {
10438
10570
  reject(
10439
- new Error(
10440
- "Query exceeded 30s timeout. Consider adding LIMIT or filtering by block_id."
10441
- )
10571
+ new Error("Query exceeded 30s timeout. Consider adding LIMIT or filtering by block_id.")
10442
10572
  );
10443
10573
  }, timeoutMs);
10444
10574
  });
@@ -10495,7 +10625,9 @@ function registerSQLTools(server, baseDir) {
10495
10625
  inputSchema: z17.object({
10496
10626
  query: z17.string().describe("SQL query to execute"),
10497
10627
  limit: z17.number().min(1).max(MAX_ROWS).default(100).describe(`Maximum rows to return (default: 100, max: ${MAX_ROWS})`),
10498
- confirm: z17.boolean().default(false).describe("Required for all mutating operations (DELETE, UPDATE, INSERT, CREATE, ALTER, DROP, TRUNCATE). Without it, returns a preview or prompt.")
10628
+ confirm: z17.boolean().default(false).describe(
10629
+ "Required for all mutating operations (DELETE, UPDATE, INSERT, CREATE, ALTER, DROP, TRUNCATE). Without it, returns a preview or prompt."
10630
+ )
10499
10631
  })
10500
10632
  },
10501
10633
  withFullSync(baseDir, async ({ query, limit, confirm }) => {
@@ -10525,10 +10657,11 @@ function registerSQLTools(server, baseDir) {
10525
10657
  );
10526
10658
  }
10527
10659
  }
10528
- return createToolOutput(
10529
- `\u26A0\uFE0F ${operation} requires confirm=true to execute.`,
10530
- { operation, query, preview: true }
10531
- );
10660
+ return createToolOutput(`\u26A0\uFE0F ${operation} requires confirm=true to execute.`, {
10661
+ operation,
10662
+ query,
10663
+ preview: true
10664
+ });
10532
10665
  }
10533
10666
  try {
10534
10667
  const conn = await getConnection(baseDir);
@@ -10538,10 +10671,10 @@ function registerSQLTools(server, baseDir) {
10538
10671
  const rwConn = await getConnection(baseDir);
10539
10672
  const result2 = await rwConn.run(query);
10540
10673
  const changed = Number(result2.rowsChanged);
10541
- return createToolOutput(
10542
- `${operation} completed: ${changed} row(s) affected.`,
10543
- { operation, rowsAffected: changed }
10544
- );
10674
+ return createToolOutput(`${operation} completed: ${changed} row(s) affected.`, {
10675
+ operation,
10676
+ rowsAffected: changed
10677
+ });
10545
10678
  } finally {
10546
10679
  await downgradeToReadOnly(baseDir);
10547
10680
  }
@@ -10688,19 +10821,17 @@ function registerSchemaTools(server, baseDir) {
10688
10821
  totalRows += rowCount;
10689
10822
  const schemaDesc = SCHEMA_DESCRIPTIONS[schemaName];
10690
10823
  const tableDesc = schemaDesc?.tables?.[tableName];
10691
- const columns = columnsData.map(
10692
- ([columnName, dataType, isNullable]) => {
10693
- const colDesc = tableDesc?.columns?.[columnName];
10694
- return {
10695
- name: columnName,
10696
- type: dataType,
10697
- description: colDesc?.description || "",
10698
- nullable: isNullable,
10699
- hypothesis: colDesc?.hypothesis || false,
10700
- timing: colDesc?.timing
10701
- };
10702
- }
10703
- );
10824
+ const columns = columnsData.map(([columnName, dataType, isNullable]) => {
10825
+ const colDesc = tableDesc?.columns?.[columnName];
10826
+ return {
10827
+ name: columnName,
10828
+ type: dataType,
10829
+ description: colDesc?.description || "",
10830
+ nullable: isNullable,
10831
+ hypothesis: colDesc?.hypothesis || false,
10832
+ timing: colDesc?.timing
10833
+ };
10834
+ });
10704
10835
  const tableInfo = {
10705
10836
  description: tableDesc?.description || `${tableName} table`,
10706
10837
  keyColumns: tableDesc?.keyColumns || [],
@@ -10773,16 +10904,12 @@ function registerSchemaTools(server, baseDir) {
10773
10904
  }
10774
10905
  try {
10775
10906
  const fullTableName = `market.${table}`;
10776
- const countResult = await conn.runAndReadAll(
10777
- `SELECT COUNT(*) FROM ${fullTableName}`
10778
- );
10907
+ const countResult = await conn.runAndReadAll(`SELECT COUNT(*) FROM ${fullTableName}`);
10779
10908
  const rowsBefore = Number(countResult.getRows()[0][0]);
10780
10909
  try {
10781
10910
  await conn.run(`BEGIN TRANSACTION`);
10782
10911
  await conn.run(`DELETE FROM ${fullTableName}`);
10783
- await conn.run(
10784
- `DELETE FROM market._sync_metadata WHERE target_table = '${table}'`
10785
- );
10912
+ await conn.run(`DELETE FROM market._sync_metadata WHERE target_table = '${table}'`);
10786
10913
  await conn.run(`COMMIT`);
10787
10914
  } catch (e) {
10788
10915
  await conn.run(`ROLLBACK`).catch(() => {
@@ -10877,12 +11004,8 @@ Worst losing streak: ${worstDesc}`;
10877
11004
  windowSize: z19.number().min(5).optional().describe(
10878
11005
  "Rolling window size in trades (default: auto-calculated based on trade count)"
10879
11006
  ),
10880
- recentWindowSize: z19.number().min(10).optional().describe(
10881
- "Recent window size in trades for comparison (default: auto-calculated)"
10882
- ),
10883
- recentWindowDays: z19.number().min(7).optional().describe(
10884
- "Override: recent window as calendar days instead of trade count"
10885
- ),
11007
+ recentWindowSize: z19.number().min(10).optional().describe("Recent window size in trades for comparison (default: auto-calculated)"),
11008
+ recentWindowDays: z19.number().min(7).optional().describe("Override: recent window as calendar days instead of trade count"),
10886
11009
  includeSeries: z19.boolean().optional().default(false).describe(
10887
11010
  "Include full rolling series data points in output (default: false, saves tokens)"
10888
11011
  )
@@ -10967,9 +11090,7 @@ Recent vs historical: win rate ${fmtPct(recentWR)} vs ${fmtPct(histWR)}, PF ${fm
10967
11090
  recentWindowSize: z19.number().min(20).optional().describe(
10968
11091
  "Number of recent trades for the recent window simulation (default: auto-calculated, typically max(20% of trades, 200))"
10969
11092
  ),
10970
- numSimulations: z19.number().min(50).max(1e4).optional().describe(
10971
- "Number of Monte Carlo simulation paths (default: 1000)"
10972
- ),
11093
+ numSimulations: z19.number().min(50).max(1e4).optional().describe("Number of Monte Carlo simulation paths (default: 1000)"),
10973
11094
  simulationLength: z19.number().min(10).optional().describe(
10974
11095
  "Number of trades to project forward per simulation (default: recentWindowSize)"
10975
11096
  ),
@@ -11010,7 +11131,9 @@ Recent vs historical: win rate ${fmtPct(recentWR)} vs ${fmtPct(histWR)}, PF ${fm
11010
11131
  // Already filtered above
11011
11132
  });
11012
11133
  const fullPProfit = (result.fullHistory.statistics.probabilityOfProfit * 100).toFixed(1);
11013
- const recentPProfit = (result.recentWindow.statistics.probabilityOfProfit * 100).toFixed(1);
11134
+ const recentPProfit = (result.recentWindow.statistics.probabilityOfProfit * 100).toFixed(
11135
+ 1
11136
+ );
11014
11137
  const fullSharpe = result.fullHistory.statistics.meanSharpeRatio.toFixed(2);
11015
11138
  const recentSharpe = result.recentWindow.statistics.meanSharpeRatio.toFixed(2);
11016
11139
  const score = result.divergence.compositeScore.toFixed(2);
@@ -11070,19 +11193,11 @@ Divergence: score ${score}`;
11070
11193
  inputSchema: z19.object({
11071
11194
  blockId: z19.string().describe("Block folder name"),
11072
11195
  strategy: z19.string().optional().describe("Filter by strategy name (case-insensitive)"),
11073
- inSampleDays: z19.number().min(30).optional().describe(
11074
- "In-sample window in calendar days (default: 365)"
11075
- ),
11076
- outOfSampleDays: z19.number().min(7).optional().describe(
11077
- "Out-of-sample window in calendar days (default: 90)"
11078
- ),
11196
+ inSampleDays: z19.number().min(30).optional().describe("In-sample window in calendar days (default: 365)"),
11197
+ outOfSampleDays: z19.number().min(7).optional().describe("Out-of-sample window in calendar days (default: 90)"),
11079
11198
  stepSizeDays: z19.number().min(7).optional().describe("Step size in calendar days (default: 90)"),
11080
- minTradesPerPeriod: z19.number().min(1).optional().describe(
11081
- "Minimum trades for a period to be considered sufficient (default: 10)"
11082
- ),
11083
- recentPeriodCount: z19.number().min(1).optional().describe(
11084
- "Number of recent WF periods for comparison (default: 3)"
11085
- )
11199
+ minTradesPerPeriod: z19.number().min(1).optional().describe("Minimum trades for a period to be considered sufficient (default: 10)"),
11200
+ recentPeriodCount: z19.number().min(1).optional().describe("Number of recent WF periods for comparison (default: 3)")
11086
11201
  })
11087
11202
  },
11088
11203
  withSyncedBlock(
@@ -11199,15 +11314,12 @@ Divergence: score ${score}`;
11199
11314
  scaling: scaling ?? "perContract"
11200
11315
  });
11201
11316
  if (!output.available) {
11202
- return createToolOutput(
11203
- `Live alignment for ${blockId}: skipped (${output.reason})`,
11204
- {
11205
- blockId,
11206
- strategy: strategy ?? null,
11207
- available: false,
11208
- reason: output.reason
11209
- }
11210
- );
11317
+ return createToolOutput(`Live alignment for ${blockId}: skipped (${output.reason})`, {
11318
+ blockId,
11319
+ strategy: strategy ?? null,
11320
+ available: false,
11321
+ reason: output.reason
11322
+ });
11211
11323
  }
11212
11324
  const result = output;
11213
11325
  const da = result.directionAgreement;
@@ -11257,69 +11369,66 @@ Divergence: score ${score}`;
11257
11369
  )
11258
11370
  })
11259
11371
  },
11260
- withSyncedBlock(
11261
- baseDir,
11262
- async ({ blockId, strategy, recentWindow }) => {
11263
- try {
11264
- const block = await loadBlock(baseDir, blockId);
11265
- const trades = applyStrategyFilter(block.trades, strategy);
11266
- if (trades.length === 0) {
11267
- return {
11268
- content: [
11269
- {
11270
- type: "text",
11271
- text: strategy ? `No trades found for strategy "${strategy}" in block "${blockId}".` : `No trades found in block "${blockId}".`
11272
- }
11273
- ],
11274
- isError: true
11275
- };
11276
- }
11277
- let actualTrades;
11278
- try {
11279
- const raw = await loadReportingLog(baseDir, blockId);
11280
- actualTrades = applyStrategyFilter(raw, strategy);
11281
- } catch {
11282
- actualTrades = void 0;
11283
- }
11284
- const result = synthesizeEdgeDecay(trades, actualTrades, {
11285
- recentWindow
11286
- });
11287
- const s = result.summary;
11288
- const fmtPct = (v) => v !== null ? (v * 100).toFixed(1) + "%" : "N/A";
11289
- const fmtRatio = (v) => v !== null ? v.toFixed(2) : "N/A";
11290
- const lines = [
11291
- `Edge decay analysis for ${blockId}${strategy ? ` (${strategy})` : ""}: ${s.totalTrades} trades, recent window=${s.recentWindow}`,
11292
- `Win rate: ${fmtPct(s.recentWinRate)} recent vs ${fmtPct(s.historicalWinRate)} historical`,
11293
- `Profit factor: ${fmtRatio(s.recentProfitFactor)} recent vs ${fmtRatio(s.historicalProfitFactor)} historical`,
11294
- `Sharpe: ${fmtRatio(s.recentSharpe)} recent vs ${fmtRatio(s.historicalSharpe)} historical`,
11295
- `Signals: ${result.metadata.signalsRun} run, ${result.metadata.signalsSkipped} skipped`,
11296
- `Observations: ${s.observationCount} notable (${s.structuralFlagCount} structural flags)`
11297
- ];
11298
- if (s.mcProbabilityOfProfit) {
11299
- lines.push(
11300
- `MC P(Profit): ${(s.mcProbabilityOfProfit.full * 100).toFixed(1)}% full vs ${(s.mcProbabilityOfProfit.recent * 100).toFixed(1)}% recent`
11301
- );
11302
- }
11303
- if (s.liveDirectionAgreement !== null) {
11304
- lines.push(
11305
- `Live alignment: ${fmtPct(s.liveDirectionAgreement)} direction agreement, ${fmtRatio(s.liveExecutionEfficiency)} efficiency`
11306
- );
11307
- }
11308
- const summaryText = lines.join("\n");
11309
- return createToolOutput(summaryText, result);
11310
- } catch (error) {
11372
+ withSyncedBlock(baseDir, async ({ blockId, strategy, recentWindow }) => {
11373
+ try {
11374
+ const block = await loadBlock(baseDir, blockId);
11375
+ const trades = applyStrategyFilter(block.trades, strategy);
11376
+ if (trades.length === 0) {
11311
11377
  return {
11312
11378
  content: [
11313
11379
  {
11314
11380
  type: "text",
11315
- text: `Error analyzing edge decay: ${error.message}`
11381
+ text: strategy ? `No trades found for strategy "${strategy}" in block "${blockId}".` : `No trades found in block "${blockId}".`
11316
11382
  }
11317
11383
  ],
11318
11384
  isError: true
11319
11385
  };
11320
11386
  }
11387
+ let actualTrades;
11388
+ try {
11389
+ const raw = await loadReportingLog(baseDir, blockId);
11390
+ actualTrades = applyStrategyFilter(raw, strategy);
11391
+ } catch {
11392
+ actualTrades = void 0;
11393
+ }
11394
+ const result = synthesizeEdgeDecay(trades, actualTrades, {
11395
+ recentWindow
11396
+ });
11397
+ const s = result.summary;
11398
+ const fmtPct = (v) => v !== null ? (v * 100).toFixed(1) + "%" : "N/A";
11399
+ const fmtRatio = (v) => v !== null ? v.toFixed(2) : "N/A";
11400
+ const lines = [
11401
+ `Edge decay analysis for ${blockId}${strategy ? ` (${strategy})` : ""}: ${s.totalTrades} trades, recent window=${s.recentWindow}`,
11402
+ `Win rate: ${fmtPct(s.recentWinRate)} recent vs ${fmtPct(s.historicalWinRate)} historical`,
11403
+ `Profit factor: ${fmtRatio(s.recentProfitFactor)} recent vs ${fmtRatio(s.historicalProfitFactor)} historical`,
11404
+ `Sharpe: ${fmtRatio(s.recentSharpe)} recent vs ${fmtRatio(s.historicalSharpe)} historical`,
11405
+ `Signals: ${result.metadata.signalsRun} run, ${result.metadata.signalsSkipped} skipped`,
11406
+ `Observations: ${s.observationCount} notable (${s.structuralFlagCount} structural flags)`
11407
+ ];
11408
+ if (s.mcProbabilityOfProfit) {
11409
+ lines.push(
11410
+ `MC P(Profit): ${(s.mcProbabilityOfProfit.full * 100).toFixed(1)}% full vs ${(s.mcProbabilityOfProfit.recent * 100).toFixed(1)}% recent`
11411
+ );
11412
+ }
11413
+ if (s.liveDirectionAgreement !== null) {
11414
+ lines.push(
11415
+ `Live alignment: ${fmtPct(s.liveDirectionAgreement)} direction agreement, ${fmtRatio(s.liveExecutionEfficiency)} efficiency`
11416
+ );
11417
+ }
11418
+ const summaryText = lines.join("\n");
11419
+ return createToolOutput(summaryText, result);
11420
+ } catch (error) {
11421
+ return {
11422
+ content: [
11423
+ {
11424
+ type: "text",
11425
+ text: `Error analyzing edge decay: ${error.message}`
11426
+ }
11427
+ ],
11428
+ isError: true
11429
+ };
11321
11430
  }
11322
- )
11431
+ })
11323
11432
  );
11324
11433
  }
11325
11434
 
@@ -11651,9 +11760,7 @@ function registerGuideTools(server) {
11651
11760
  if (subtopic) {
11652
11761
  const keyword = subtopic.toLowerCase();
11653
11762
  const sections = content.split(/\n(?=## )/);
11654
- const matched = sections.filter(
11655
- (s) => s.toLowerCase().includes(keyword)
11656
- );
11763
+ const matched = sections.filter((s) => s.toLowerCase().includes(keyword));
11657
11764
  if (matched.length > 0) {
11658
11765
  content = matched.join("\n\n");
11659
11766
  }
@@ -11695,9 +11802,13 @@ var profileStrategySchema = z21.object({
11695
11802
  operator: z21.string().describe("Comparison operator: >, <, >=, <=, ==, between, in"),
11696
11803
  value: z21.union([z21.string(), z21.number(), z21.array(z21.union([z21.string(), z21.number()]))]).describe("Filter value or array for between/in operators"),
11697
11804
  description: z21.string().optional().describe("Human-readable description of this filter"),
11698
- source: z21.enum(["market", "execution"]).optional().describe("Filter source: 'market' = testable against market data columns, 'execution' = platform-level (time windows, leg ratios). Defaults to 'market'. Execution filters are documented but skipped during validate_entry_filters analysis.")
11805
+ source: z21.enum(["market", "execution"]).optional().describe(
11806
+ "Filter source: 'market' = testable against market data columns, 'execution' = platform-level (time windows, leg ratios). Defaults to 'market'. Execution filters are documented but skipped during validate_entry_filters analysis."
11807
+ )
11699
11808
  })
11700
- ).default([]).describe("Entry condition filters. Tag each with source: 'market' (testable in analysis) or 'execution' (OO/platform-level, skipped in analysis)."),
11809
+ ).default([]).describe(
11810
+ "Entry condition filters. Tag each with source: 'market' (testable in analysis) or 'execution' (OO/platform-level, skipped in analysis)."
11811
+ ),
11701
11812
  exitRules: z21.array(
11702
11813
  z21.object({
11703
11814
  type: z21.string().describe("Rule type: stop_loss, profit_target, time_exit, conditional"),
@@ -11712,7 +11823,9 @@ var profileStrategySchema = z21.object({
11712
11823
  slippage: z21.number().optional().describe("Per-rule slippage override")
11713
11824
  })
11714
11825
  ).default([]).describe("Exit rules and triggers"),
11715
- expectedRegimes: z21.array(z21.enum(["very_low", "low", "below_avg", "above_avg", "high", "extreme"])).default([]).describe("VIX-based vol regimes this strategy targets. very_low=VIX<13, low=13-16, below_avg=16-20, above_avg=20-25, high=25-30, extreme=30+"),
11826
+ expectedRegimes: z21.array(z21.enum(["very_low", "low", "below_avg", "above_avg", "high", "extreme"])).default([]).describe(
11827
+ "VIX-based vol regimes this strategy targets. very_low=VIX<13, low=13-16, below_avg=16-20, above_avg=20-25, high=25-30, extreme=30+"
11828
+ ),
11716
11829
  keyMetrics: z21.object({
11717
11830
  expectedWinRate: z21.number().optional().describe("Expected win rate (0-1)"),
11718
11831
  targetPremium: z21.number().optional().describe("Target premium collected ($)"),
@@ -11729,7 +11842,9 @@ var profileStrategySchema = z21.object({
11729
11842
  backtestAllocationPct: z21.number().optional().describe("Allocation % used in backtest"),
11730
11843
  liveAllocationPct: z21.number().optional().describe("Allocation % used in live portfolio"),
11731
11844
  maxContractsPerTrade: z21.number().optional().describe("Per-entry contract cap (distinct from maxContracts hard cap)")
11732
- }).optional().describe("Position sizing rules. Per-block \u2014 same strategy in backtest vs portfolio may have different sizing."),
11845
+ }).optional().describe(
11846
+ "Position sizing rules. Per-block \u2014 same strategy in backtest vs portfolio may have different sizing."
11847
+ ),
11733
11848
  underlying: z21.string().optional().describe("Underlying symbol: SPX, QQQ, etc."),
11734
11849
  reEntry: z21.boolean().optional().describe("Strategy supports re-entry on same day"),
11735
11850
  capProfits: z21.boolean().optional().describe("Profits are capped by structure"),
@@ -11753,30 +11868,33 @@ async function handleProfileStrategy(input, baseDir) {
11753
11868
  await upgradeToReadWrite(baseDir);
11754
11869
  try {
11755
11870
  const conn = await getConnection(baseDir);
11756
- const stored = await upsertProfile(conn, {
11757
- blockId: input.blockId,
11758
- strategyName: input.strategyName,
11759
- structureType: input.structureType,
11760
- greeksBias: input.greeksBias,
11761
- thesis: input.thesis,
11762
- legs: input.legs,
11763
- entryFilters: input.entryFilters,
11764
- exitRules: input.exitRules,
11765
- expectedRegimes: input.expectedRegimes,
11766
- keyMetrics: input.keyMetrics,
11767
- positionSizing: input.positionSizing,
11768
- underlying: input.underlying,
11769
- reEntry: input.reEntry,
11770
- capProfits: input.capProfits,
11771
- capLosses: input.capLosses,
11772
- requireTwoPricesPT: input.requireTwoPricesPT,
11773
- closeOnCompletion: input.closeOnCompletion,
11774
- ignoreMarginReq: input.ignoreMarginReq
11775
- }, baseDir);
11776
- return createToolOutput(
11777
- `Profile saved: ${input.strategyName} for block ${input.blockId}`,
11778
- { profile: stored }
11871
+ const stored = await upsertProfile(
11872
+ conn,
11873
+ {
11874
+ blockId: input.blockId,
11875
+ strategyName: input.strategyName,
11876
+ structureType: input.structureType,
11877
+ greeksBias: input.greeksBias,
11878
+ thesis: input.thesis,
11879
+ legs: input.legs,
11880
+ entryFilters: input.entryFilters,
11881
+ exitRules: input.exitRules,
11882
+ expectedRegimes: input.expectedRegimes,
11883
+ keyMetrics: input.keyMetrics,
11884
+ positionSizing: input.positionSizing,
11885
+ underlying: input.underlying,
11886
+ reEntry: input.reEntry,
11887
+ capProfits: input.capProfits,
11888
+ capLosses: input.capLosses,
11889
+ requireTwoPricesPT: input.requireTwoPricesPT,
11890
+ closeOnCompletion: input.closeOnCompletion,
11891
+ ignoreMarginReq: input.ignoreMarginReq
11892
+ },
11893
+ baseDir
11779
11894
  );
11895
+ return createToolOutput(`Profile saved: ${input.strategyName} for block ${input.blockId}`, {
11896
+ profile: stored
11897
+ });
11780
11898
  } finally {
11781
11899
  await downgradeToReadOnly(baseDir);
11782
11900
  }
@@ -11790,10 +11908,7 @@ async function handleGetStrategyProfile(input, baseDir) {
11790
11908
  { profile: null }
11791
11909
  );
11792
11910
  }
11793
- return createToolOutput(
11794
- `Profile: ${input.strategyName} in block ${input.blockId}`,
11795
- { profile }
11796
- );
11911
+ return createToolOutput(`Profile: ${input.strategyName} in block ${input.blockId}`, { profile });
11797
11912
  }
11798
11913
  async function handleListProfiles(input, baseDir) {
11799
11914
  const conn = await getConnection(baseDir);
@@ -11860,9 +11975,12 @@ function registerProfileTools(server, baseDir) {
11860
11975
  },
11861
11976
  async (input) => {
11862
11977
  if (input.blockId) {
11863
- const syncedHandler = withSyncedBlock(baseDir, async (syncInput, ctx) => {
11864
- return handleListProfiles({ blockId: syncInput.blockId }, ctx.baseDir);
11865
- });
11978
+ const syncedHandler = withSyncedBlock(
11979
+ baseDir,
11980
+ async (syncInput, ctx) => {
11981
+ return handleListProfiles({ blockId: syncInput.blockId }, ctx.baseDir);
11982
+ }
11983
+ );
11866
11984
  return syncedHandler({ blockId: input.blockId });
11867
11985
  }
11868
11986
  return handleListProfiles(input, baseDir);
@@ -12539,7 +12657,9 @@ async function handleValidateEntryFilters(input, baseDir) {
12539
12657
  single: ablationSingle,
12540
12658
  pairs: ablationPairs
12541
12659
  },
12542
- execution_filters_skipped: executionFilters.map((f) => f.description || `${f.field} ${f.operator} ${f.value}`),
12660
+ execution_filters_skipped: executionFilters.map(
12661
+ (f) => f.description || `${f.field} ${f.operator} ${f.value}`
12662
+ ),
12543
12663
  profile_update_hints: profileUpdateHints,
12544
12664
  warnings
12545
12665
  });
@@ -12570,7 +12690,9 @@ async function handlePortfolioStructureMap(input, baseDir) {
12570
12690
  try {
12571
12691
  block = await loadBlock(baseDir, profile.blockId);
12572
12692
  } catch {
12573
- warnings.push(`Could not load block '${profile.blockId}' for strategy '${profile.strategyName}'`);
12693
+ warnings.push(
12694
+ `Could not load block '${profile.blockId}' for strategy '${profile.strategyName}'`
12695
+ );
12574
12696
  continue;
12575
12697
  }
12576
12698
  let trades = filterByStrategy(block.trades, profile.strategyName);
@@ -12581,7 +12703,9 @@ async function handlePortfolioStructureMap(input, baseDir) {
12581
12703
  }
12582
12704
  }
12583
12705
  if (trades.length === 0) {
12584
- warnings.push(`No trades found for strategy '${profile.strategyName}' in block '${profile.blockId}'`);
12706
+ warnings.push(
12707
+ `No trades found for strategy '${profile.strategyName}' in block '${profile.blockId}'`
12708
+ );
12585
12709
  continue;
12586
12710
  }
12587
12711
  const tradeKeys = uniqueTradeLookupKeys3(trades);
@@ -12699,10 +12823,7 @@ async function handlePortfolioStructureMap(input, baseDir) {
12699
12823
  );
12700
12824
  }
12701
12825
  const unknownTrendStats = unknownTrendPls.size > 0 ? Object.fromEntries(
12702
- [...unknownTrendPls.entries()].map(([name, pls]) => [
12703
- name,
12704
- computeSliceStats(pls)
12705
- ])
12826
+ [...unknownTrendPls.entries()].map(([name, pls]) => [name, computeSliceStats(pls)])
12706
12827
  ) : void 0;
12707
12828
  const coverageSummary = {
12708
12829
  totalCells: 18,
@@ -12747,7 +12868,7 @@ function registerProfileAnalysisTools(server, baseDir) {
12747
12868
  if (getConnectionMode() === "read_write") {
12748
12869
  try {
12749
12870
  if (input.blockId) {
12750
- const { syncBlock: syncBlock2 } = await import("./sync-QFI5L7S7.js");
12871
+ const { syncBlock: syncBlock2 } = await import("./sync-V25UQJA3.js");
12751
12872
  await syncBlock2(input.blockId, baseDir);
12752
12873
  } else {
12753
12874
  await syncAllBlocks(baseDir);
@@ -12849,12 +12970,8 @@ var VOL_REGIME_LABELS3 = {
12849
12970
  6: "extreme"
12850
12971
  };
12851
12972
  var regimeAllocationAdvisorSchema = z23.object({
12852
- blockId: z23.string().optional().describe(
12853
- "Block ID to analyze. When omitted, aggregate across all profiled strategies."
12854
- ),
12855
- minTrades: z23.number().optional().default(5).describe(
12856
- "Minimum trades per regime cell for reliable stats (default: 5)"
12857
- )
12973
+ blockId: z23.string().optional().describe("Block ID to analyze. When omitted, aggregate across all profiled strategies."),
12974
+ minTrades: z23.number().optional().default(5).describe("Minimum trades per regime cell for reliable stats (default: 5)")
12858
12975
  });
12859
12976
  async function handleRegimeAllocationAdvisor(input, baseDir) {
12860
12977
  const minTrades = input.minTrades ?? 5;
@@ -12946,9 +13063,7 @@ async function handleRegimeAllocationAdvisor(input, baseDir) {
12946
13063
  if (!regimeAggPls[label]) regimeAggPls[label] = [];
12947
13064
  regimeAggPls[label].push(trade.pl);
12948
13065
  }
12949
- const expectedSet = new Set(
12950
- profile.expectedRegimes.map((r) => r.toLowerCase())
12951
- );
13066
+ const expectedSet = new Set(profile.expectedRegimes.map((r) => r.toLowerCase()));
12952
13067
  const regimePerformance = {};
12953
13068
  for (const [label, pls] of Object.entries(regimePls)) {
12954
13069
  const stats = computeSliceStats(pls);
@@ -13044,7 +13159,7 @@ function registerRegimeAdvisorTools(server, baseDir) {
13044
13159
  if (getConnectionMode() === "read_write") {
13045
13160
  try {
13046
13161
  if (input.blockId) {
13047
- const { syncBlock: syncBlock2 } = await import("./sync-QFI5L7S7.js");
13162
+ const { syncBlock: syncBlock2 } = await import("./sync-V25UQJA3.js");
13048
13163
  await syncBlock2(input.blockId, baseDir);
13049
13164
  } else {
13050
13165
  await syncAllBlocks(baseDir);
@@ -13088,9 +13203,7 @@ var replayTradeSchema = z24.object({
13088
13203
  ).optional().describe("Explicit leg definitions for hypothetical replay"),
13089
13204
  // Mode B: Tradelog replay
13090
13205
  block_id: z24.string().optional().describe("Block ID to load trade from"),
13091
- trade_index: z24.number().optional().describe(
13092
- "0-based index of trade in block's tradelog (ordered by date_opened)"
13093
- ),
13206
+ trade_index: z24.number().optional().describe("0-based index of trade in block's tradelog (ordered by date_opened)"),
13094
13207
  // Common fields
13095
13208
  open_date: z24.string().optional().describe(
13096
13209
  "Trade open date YYYY-MM-DD (required for hypothetical mode, auto-resolved for tradelog mode)"
@@ -13137,22 +13250,13 @@ function resolveOODateRange(parsedLegs, tradeYear, tradeOpenDate) {
13137
13250
  return { from: tradeOpenDate, to: maxDate };
13138
13251
  }
13139
13252
  async function handleReplayTrade(params, baseDir, stores, injectedConn) {
13140
- const {
13141
- legs: inputLegs,
13142
- block_id,
13143
- trade_index,
13144
- multiplier,
13145
- close_at,
13146
- skip_quotes
13147
- } = params;
13253
+ const { legs: inputLegs, block_id, trade_index, multiplier, close_at, skip_quotes } = params;
13148
13254
  let { open_date, close_date } = params;
13149
13255
  let tradeCloseTimestamp;
13150
13256
  let replayLegs;
13151
13257
  if (inputLegs && inputLegs.length > 0) {
13152
13258
  if (!open_date || !close_date) {
13153
- throw new Error(
13154
- "open_date and close_date are required for hypothetical replay mode"
13155
- );
13259
+ throw new Error("open_date and close_date are required for hypothetical replay mode");
13156
13260
  }
13157
13261
  replayLegs = inputLegs.map((leg) => ({
13158
13262
  occTicker: buildOccTicker(leg.ticker, leg.expiry, leg.type, leg.strike),
@@ -13171,9 +13275,7 @@ async function handleReplayTrade(params, baseDir, stores, injectedConn) {
13171
13275
  );
13172
13276
  const rows = result.getRows();
13173
13277
  if (rows.length === 0) {
13174
- throw new Error(
13175
- `No trade found at index ${trade_index} in block "${block_id}"`
13176
- );
13278
+ throw new Error(`No trade found at index ${trade_index} in block "${block_id}"`);
13177
13279
  }
13178
13280
  const row = rows[0];
13179
13281
  const legsStr = String(row[0] ?? "");
@@ -13282,11 +13384,7 @@ async function handleReplayTrade(params, baseDir, stores, injectedConn) {
13282
13384
  );
13283
13385
  if (underlyingBars.length === 0) {
13284
13386
  try {
13285
- underlyingBars = await stores.spot.readDailyBars(
13286
- underlyingTicker,
13287
- open_date,
13288
- close_date
13289
- );
13387
+ underlyingBars = await stores.spot.readDailyBars(underlyingTicker, open_date, close_date);
13290
13388
  } catch {
13291
13389
  }
13292
13390
  }
@@ -13439,9 +13537,7 @@ var getOptionSnapshotSchema = z25.object({
13439
13537
  expiration_date_gte: z25.string().optional().describe("Earliest expiration date (YYYY-MM-DD)"),
13440
13538
  expiration_date_lte: z25.string().optional().describe("Latest expiration date (YYYY-MM-DD)"),
13441
13539
  contract_type: z25.enum(["call", "put"]).optional().describe("Filter by call or put"),
13442
- limit: z25.number().optional().default(50).describe(
13443
- "Max contracts to return (default 50, use higher for full chain)"
13444
- )
13540
+ limit: z25.number().optional().default(50).describe("Max contracts to return (default 50, use higher for full chain)")
13445
13541
  });
13446
13542
  async function handleGetOptionSnapshot(params) {
13447
13543
  try {
@@ -13611,7 +13707,7 @@ function evaluateTrigger(trigger, pnlPath, legs) {
13611
13707
  switch (type) {
13612
13708
  case "profitTarget": {
13613
13709
  if (trigger.unit === "percent" && trigger.entryCost == null) break;
13614
- const requiredHits = trigger.requiredHits ?? 1;
13710
+ const requiredHits = trigger.requiredHits ?? 2;
13615
13711
  const dollarThresholdPT = trigger.unit === "percent" ? threshold * Math.abs(trigger.entryCost) : threshold;
13616
13712
  if (pnl >= dollarThresholdPT) {
13617
13713
  if (point.allLegsSync !== false) profitTargetHits++;
@@ -14019,7 +14115,9 @@ function numericalDecomposition(config, totalPnlChange, stepCount) {
14019
14115
  ...f,
14020
14116
  pctOfTotal: totalAbsSum > 0 ? Math.abs(f.totalPnl) / totalAbsSum * 100 : 0
14021
14117
  }));
14022
- const summaryParts = factors.map((f) => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`);
14118
+ const summaryParts = factors.map(
14119
+ (f) => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`
14120
+ );
14023
14121
  const summary = `P&L of ${totalPnlChange.toFixed(2)} (numerical): ${summaryParts.join(", ")}`;
14024
14122
  return {
14025
14123
  factors,
@@ -14054,7 +14152,15 @@ function priceOption(type, S, K, dte, r, q, iv) {
14054
14152
  return bsPrice(bsType, S, K, T, r, q, iv);
14055
14153
  }
14056
14154
  function decomposeGreeks(config) {
14057
- const { pnlPath, legs, underlyingPrices, legGroups, legPricingInputs, riskFreeRate, dividendYield } = config;
14155
+ const {
14156
+ pnlPath,
14157
+ legs,
14158
+ underlyingPrices,
14159
+ legGroups,
14160
+ legPricingInputs,
14161
+ riskFreeRate,
14162
+ dividendYield
14163
+ } = config;
14058
14164
  if (pnlPath.length <= 1) {
14059
14165
  const emptyFactors = [
14060
14166
  { factor: "delta", totalPnl: 0, pctOfTotal: 0, steps: [] },
@@ -14214,7 +14320,9 @@ function decomposeGreeks(config) {
14214
14320
  const summaryParts = factors.filter((f) => f.factor !== "residual").map((f) => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`);
14215
14321
  const residualFactor = factors.find((f) => f.factor === "residual");
14216
14322
  if (residualFactor && Math.abs(residualFactor.totalPnl) > 0.01) {
14217
- summaryParts.push(`residual ${residualFactor.totalPnl.toFixed(2)} (${residualFactor.pctOfTotal.toFixed(0)}%)`);
14323
+ summaryParts.push(
14324
+ `residual ${residualFactor.totalPnl.toFixed(2)} (${residualFactor.pctOfTotal.toFixed(0)}%)`
14325
+ );
14218
14326
  }
14219
14327
  const summary = `P&L of ${totalPnlChange.toFixed(2)} (${methodLabel}): ${summaryParts.join(", ")}`;
14220
14328
  const warning = residualPct > 0.5 ? `Residual ${(residualPct * 100).toFixed(0)}% \u2014 attribution limited for some legs.` : null;
@@ -14257,11 +14365,13 @@ var triggerConfigSchema = z26.object({
14257
14365
  openDate: z26.string().optional(),
14258
14366
  clockTime: z26.string().optional(),
14259
14367
  trailAmount: z26.number().optional(),
14260
- steps: z26.array(z26.object({
14261
- armAt: z26.number(),
14262
- stopAt: z26.number(),
14263
- closeAllocationPct: z26.number().min(0).max(1).optional().describe("Fraction of REMAINING position to close at this milestone (0-1)")
14264
- })).optional(),
14368
+ steps: z26.array(
14369
+ z26.object({
14370
+ armAt: z26.number(),
14371
+ stopAt: z26.number(),
14372
+ closeAllocationPct: z26.number().min(0).max(1).optional().describe("Fraction of REMAINING position to close at this milestone (0-1)")
14373
+ })
14374
+ ).optional(),
14265
14375
  spreadWidth: z26.number().optional(),
14266
14376
  contracts: z26.number().optional(),
14267
14377
  legIndex: z26.number().optional().describe("0-based leg index for perLegDelta \u2014 targets specific leg"),
@@ -14286,11 +14396,13 @@ var analyzeExitTriggersSchema = z26.object({
14286
14396
  multiplier: z26.number().default(100),
14287
14397
  triggers: z26.array(triggerConfigSchema).describe("Exit triggers to evaluate against the P&L path"),
14288
14398
  actual_exit_timestamp: z26.string().optional().describe("Actual exit time for comparison (format: YYYY-MM-DD HH:MM)"),
14289
- leg_groups: z26.array(z26.object({
14290
- label: z26.string(),
14291
- leg_indices: z26.array(z26.number()),
14292
- triggers: z26.array(triggerConfigSchema)
14293
- })).optional().describe("Per-leg-group exit triggers for multi-structure strategies"),
14399
+ leg_groups: z26.array(
14400
+ z26.object({
14401
+ label: z26.string(),
14402
+ leg_indices: z26.array(z26.number()),
14403
+ triggers: z26.array(triggerConfigSchema)
14404
+ })
14405
+ ).optional().describe("Per-leg-group exit triggers for multi-structure strategies"),
14294
14406
  format: z26.enum(["summary", "full"]).default("summary").describe("'summary' omits per-step trigger states, 'full' includes all fire events")
14295
14407
  });
14296
14408
  var decomposeGreeksSchema = z26.object({
@@ -14301,10 +14413,12 @@ var decomposeGreeksSchema = z26.object({
14301
14413
  open_date: z26.string().optional(),
14302
14414
  close_date: z26.string().optional(),
14303
14415
  multiplier: z26.number().default(100),
14304
- leg_groups: z26.array(z26.object({
14305
- label: z26.string(),
14306
- leg_indices: z26.array(z26.number())
14307
- })).optional().describe("Leg grouping for per-group vega attribution (e.g., front_month vs back_month)"),
14416
+ leg_groups: z26.array(
14417
+ z26.object({
14418
+ label: z26.string(),
14419
+ leg_indices: z26.array(z26.number())
14420
+ })
14421
+ ).optional().describe("Leg grouping for per-group vega attribution (e.g., front_month vs back_month)"),
14308
14422
  format: z26.enum(["summary", "full"]).default("summary").describe("'summary' shows ranked factors, 'full' includes per-step contributions"),
14309
14423
  skip_quotes: z26.boolean().default(false).describe("Skip NBBO quote enrichment for option bars. Faster, but lower precision.")
14310
14424
  });
@@ -14329,7 +14443,8 @@ async function fetchPriceMap(stores, ticker, from, to) {
14329
14443
  }
14330
14444
  }
14331
14445
  for (const b of bars) {
14332
- if (!Number.isFinite(b.open) || b.open <= 0 || !Number.isFinite(b.high) || b.high <= 0 || !Number.isFinite(b.low) || b.low <= 0 || !Number.isFinite(b.close) || b.close <= 0) continue;
14446
+ if (!Number.isFinite(b.open) || b.open <= 0 || !Number.isFinite(b.high) || b.high <= 0 || !Number.isFinite(b.low) || b.low <= 0 || !Number.isFinite(b.close) || b.close <= 0)
14447
+ continue;
14333
14448
  const ts = `${b.date} ${b.time ?? ""}`.trim();
14334
14449
  map.set(ts, markPrice(b));
14335
14450
  }
@@ -14400,12 +14515,7 @@ async function handleAnalyzeExitTriggers(params, baseDir, stores, injectedConn)
14400
14515
  vix9dPrices = await fetchPriceMap(stores, "VIX9D", firstDate, lastDate);
14401
14516
  }
14402
14517
  if (needsUnderlying) {
14403
- underlyingPrices = await fetchPriceMap(
14404
- stores,
14405
- underlyingTicker,
14406
- firstDate,
14407
- lastDate
14408
- );
14518
+ underlyingPrices = await fetchPriceMap(stores, underlyingTicker, firstDate, lastDate);
14409
14519
  }
14410
14520
  const exitTriggers = triggers.map((t) => ({
14411
14521
  type: t.type,
@@ -14827,11 +14937,13 @@ var triggerConfigSchema2 = z27.object({
14827
14937
  openDate: z27.string().optional(),
14828
14938
  clockTime: z27.string().optional(),
14829
14939
  trailAmount: z27.number().optional(),
14830
- steps: z27.array(z27.object({
14831
- armAt: z27.number(),
14832
- stopAt: z27.number(),
14833
- closeAllocationPct: z27.number().min(0).max(1).optional().describe("Fraction of REMAINING position to close at this milestone (0-1)")
14834
- })).optional(),
14940
+ steps: z27.array(
14941
+ z27.object({
14942
+ armAt: z27.number(),
14943
+ stopAt: z27.number(),
14944
+ closeAllocationPct: z27.number().min(0).max(1).optional().describe("Fraction of REMAINING position to close at this milestone (0-1)")
14945
+ })
14946
+ ).optional(),
14835
14947
  spreadWidth: z27.number().optional(),
14836
14948
  contracts: z27.number().optional(),
14837
14949
  legIndex: z27.number().optional().describe("0-based leg index for perLegDelta \u2014 targets specific leg"),
@@ -14846,17 +14958,23 @@ var batchExitAnalysisSchema = z27.object({
14846
14958
  to: z27.string().optional().describe("End date YYYY-MM-DD")
14847
14959
  }).optional().describe("Filter trades by date range"),
14848
14960
  candidate_policy: z27.array(triggerConfigSchema2).describe("Candidate exit policy triggers to evaluate -- same schema as analyze_exit_triggers"),
14849
- leg_groups: z27.array(z27.object({
14850
- label: z27.string(),
14851
- leg_indices: z27.array(z27.number()),
14852
- triggers: z27.array(triggerConfigSchema2)
14853
- })).optional().describe("Per-leg-group exit triggers for multi-structure strategies"),
14854
- baseline_mode: z27.enum(["actual", "holdToEnd"]).default("actual").describe("'actual' compares candidate vs trade's actual P&L; 'holdToEnd' compares vs last replay timestamp"),
14961
+ leg_groups: z27.array(
14962
+ z27.object({
14963
+ label: z27.string(),
14964
+ leg_indices: z27.array(z27.number()),
14965
+ triggers: z27.array(triggerConfigSchema2)
14966
+ })
14967
+ ).optional().describe("Per-leg-group exit triggers for multi-structure strategies"),
14968
+ baseline_mode: z27.enum(["actual", "holdToEnd"]).default("actual").describe(
14969
+ "'actual' compares candidate vs trade's actual P&L; 'holdToEnd' compares vs last replay timestamp"
14970
+ ),
14855
14971
  limit: z27.number().min(1).max(200).default(50).describe("Max trades to analyze. Most recent trades selected"),
14856
14972
  min_pl: z27.number().optional().describe("Only include trades with actual P&L >= this value"),
14857
14973
  max_pl: z27.number().optional().describe("Only include trades with actual P&L <= this value"),
14858
14974
  multiplier: z27.number().default(100).describe("Contract multiplier (default 100)"),
14859
- format: z27.enum(["summary", "full"]).default("summary").describe("'summary' returns aggregate stats + trigger attribution; 'full' adds per-trade breakdown")
14975
+ format: z27.enum(["summary", "full"]).default("summary").describe(
14976
+ "'summary' returns aggregate stats + trigger attribution; 'full' adds per-trade breakdown"
14977
+ )
14860
14978
  });
14861
14979
  async function handleBatchExitAnalysis(params, baseDir, stores, injectedConn) {
14862
14980
  const {
@@ -15019,9 +15137,7 @@ async function handleBatchExitAnalysis(params, baseDir, stores, injectedConn) {
15019
15137
  if (profile) {
15020
15138
  result.profileContext = {
15021
15139
  structureType: profile.structureType,
15022
- exitRules: profile.exitRules.map(
15023
- (r) => r.description ?? `${r.type} ${r.trigger}`
15024
- )
15140
+ exitRules: profile.exitRules.map((r) => r.description ?? `${r.type} ${r.trigger}`)
15025
15141
  };
15026
15142
  }
15027
15143
  } catch {
@@ -15042,10 +15158,12 @@ function registerBatchExitAnalysisTools(server, baseDir, stores) {
15042
15158
  return createToolOutput(result.summary, result);
15043
15159
  } catch (error) {
15044
15160
  return {
15045
- content: [{
15046
- type: "text",
15047
- text: `Error in batch exit analysis: ${error.message}`
15048
- }],
15161
+ content: [
15162
+ {
15163
+ type: "text",
15164
+ text: `Error in batch exit analysis: ${error.message}`
15165
+ }
15166
+ ],
15049
15167
  isError: true
15050
15168
  };
15051
15169
  }
@@ -15075,8 +15193,12 @@ function tradingDays(from, to) {
15075
15193
  var getGreeksAttributionSchema = z28.object({
15076
15194
  block_id: z28.string().describe("Block ID to analyze"),
15077
15195
  mode: z28.enum(["summary", "instance"]).default("summary").describe("summary: block-level attribution. instance: single trade time-series."),
15078
- trade_index: z28.number().int().min(0).optional().describe("Trade index (required for instance mode). Use get_block_info to find trade indices."),
15079
- skip_quotes: z28.boolean().default(true).describe("Use cached bar data only (fast). Set false to fetch NBBO quotes for higher precision."),
15196
+ trade_index: z28.number().int().min(0).optional().describe(
15197
+ "Trade index (required for instance mode). Use get_block_info to find trade indices."
15198
+ ),
15199
+ skip_quotes: z28.boolean().default(true).describe(
15200
+ "Use cached bar data only (fast). Set false to fetch NBBO quotes for higher precision."
15201
+ ),
15080
15202
  detailed: z28.boolean().default(false).describe("false: 5 factors (delta, gamma, theta, vega, residual). true: adds charm, vanna."),
15081
15203
  strategy: z28.string().optional().describe("Filter to trades matching this strategy name (case-insensitive).")
15082
15204
  });
@@ -15084,7 +15206,16 @@ var COLLAPSE_MAP = {
15084
15206
  charm: "delta",
15085
15207
  vanna: "vega"
15086
15208
  };
15087
- var FACTOR_ORDER = ["theta", "vega", "delta", "gamma", "residual", "time_and_vol", "charm", "vanna"];
15209
+ var FACTOR_ORDER = [
15210
+ "theta",
15211
+ "vega",
15212
+ "delta",
15213
+ "gamma",
15214
+ "residual",
15215
+ "time_and_vol",
15216
+ "charm",
15217
+ "vanna"
15218
+ ];
15088
15219
  function collapseFactors(factors, detailed) {
15089
15220
  const totals = /* @__PURE__ */ new Map();
15090
15221
  for (const f of factors) {
@@ -15134,9 +15265,25 @@ async function handleGetGreeksAttribution(params, baseDir, stores, injectedConn)
15134
15265
  if (trade_index == null) {
15135
15266
  throw new Error("trade_index is required for instance mode");
15136
15267
  }
15137
- return handleInstanceMode(block_id, trade_index, skip_quotes, detailed, baseDir, stores, injectedConn);
15268
+ return handleInstanceMode(
15269
+ block_id,
15270
+ trade_index,
15271
+ skip_quotes,
15272
+ detailed,
15273
+ baseDir,
15274
+ stores,
15275
+ injectedConn
15276
+ );
15138
15277
  }
15139
- return handleSummaryMode(block_id, skip_quotes, detailed, strategy, baseDir, stores, injectedConn);
15278
+ return handleSummaryMode(
15279
+ block_id,
15280
+ skip_quotes,
15281
+ detailed,
15282
+ strategy,
15283
+ baseDir,
15284
+ stores,
15285
+ injectedConn
15286
+ );
15140
15287
  }
15141
15288
  async function handleSummaryMode(block_id, skip_quotes, detailed, strategy, baseDir, stores, injectedConn) {
15142
15289
  const conn = injectedConn ?? await getConnection(baseDir);
@@ -15188,7 +15335,10 @@ async function handleSummaryMode(block_id, skip_quotes, detailed, strategy, base
15188
15335
  injectedConn
15189
15336
  ).then((result) => {
15190
15337
  for (const factor of result.factors) {
15191
- accumulated.set(factor.factor, (accumulated.get(factor.factor) ?? 0) + factor.totalPnl);
15338
+ accumulated.set(
15339
+ factor.factor,
15340
+ (accumulated.get(factor.factor) ?? 0) + factor.totalPnl
15341
+ );
15192
15342
  }
15193
15343
  actualTotalPnl += trade.actualPl;
15194
15344
  markTotalPnl += result.totalPnlChange;
@@ -15291,10 +15441,20 @@ async function handleInstanceMode(block_id, trade_index, skip_quotes, detailed,
15291
15441
  for (let i = 0; i <= stepCount; i++) {
15292
15442
  const entry = {
15293
15443
  date: getStepDate(i),
15294
- delta: getStepValue(factorSteps, "delta", i, detailed ? 0 : factorSteps.get("charm")?.[i] ?? 0),
15444
+ delta: getStepValue(
15445
+ factorSteps,
15446
+ "delta",
15447
+ i,
15448
+ detailed ? 0 : factorSteps.get("charm")?.[i] ?? 0
15449
+ ),
15295
15450
  gamma: getStepValue(factorSteps, "gamma", i, 0),
15296
15451
  theta: getStepValue(factorSteps, "theta", i, 0),
15297
- vega: getStepValue(factorSteps, "vega", i, detailed ? 0 : factorSteps.get("vanna")?.[i] ?? 0),
15452
+ vega: getStepValue(
15453
+ factorSteps,
15454
+ "vega",
15455
+ i,
15456
+ detailed ? 0 : factorSteps.get("vanna")?.[i] ?? 0
15457
+ ),
15298
15458
  residual: getStepValue(factorSteps, "residual", i, 0)
15299
15459
  };
15300
15460
  if (factorSteps.has("time_and_vol")) {
@@ -15456,7 +15616,13 @@ var MarketIngestor = class {
15456
15616
  assetClass
15457
15617
  });
15458
15618
  } catch (error) {
15459
- const mapped = this.mapProviderFailure(provider, "bars", normalizedTicker, error, assetClass);
15619
+ const mapped = this.mapProviderFailure(
15620
+ provider,
15621
+ "bars",
15622
+ normalizedTicker,
15623
+ error,
15624
+ assetClass
15625
+ );
15460
15626
  if (mapped) return mapped;
15461
15627
  throw error;
15462
15628
  }
@@ -15709,11 +15875,7 @@ var MarketIngestor = class {
15709
15875
  if (mapped) return mapped;
15710
15876
  throw error;
15711
15877
  }
15712
- const written = await this.writeQuotesForTicker(
15713
- provider,
15714
- ticker,
15715
- quotes
15716
- );
15878
+ const written = await this.writeQuotesForTicker(provider, ticker, quotes);
15717
15879
  totalRows += written.rowsWritten;
15718
15880
  if (written.minDate && (!minDate || written.minDate < minDate)) minDate = written.minDate;
15719
15881
  if (written.maxDate && (!maxDate || written.maxDate > maxDate)) maxDate = written.maxDate;
@@ -15749,12 +15911,7 @@ var MarketIngestor = class {
15749
15911
  for (const underlying of underlyings) {
15750
15912
  const upperUnderlying = underlying.toUpperCase();
15751
15913
  for (const date of dates) {
15752
- const drain = await this.drainBulkQuotes(
15753
- provider,
15754
- upperUnderlying,
15755
- date,
15756
- onProgress
15757
- );
15914
+ const drain = await this.drainBulkQuotes(provider, upperUnderlying, date, onProgress);
15758
15915
  if (drain.rowsWritten > 0) {
15759
15916
  totalRows += drain.rowsWritten;
15760
15917
  if (!minDate || date < minDate) minDate = date;
@@ -15814,7 +15971,11 @@ var MarketIngestor = class {
15814
15971
  totalContracts: info.totalContracts
15815
15972
  });
15816
15973
  } : void 0;
15817
- const stream = provider.fetchBulkQuotes({ underlying: upperUnderlying, date, onGroupComplete });
15974
+ const stream = provider.fetchBulkQuotes({
15975
+ underlying: upperUnderlying,
15976
+ date,
15977
+ onGroupComplete
15978
+ });
15818
15979
  for await (const chunk of stream) {
15819
15980
  for (const row of chunk) {
15820
15981
  const root = extractRoot(row.ticker);
@@ -15865,15 +16026,12 @@ var MarketIngestor = class {
15865
16026
  );
15866
16027
  } catch (error) {
15867
16028
  const message = error instanceof Error ? error.message : String(error);
15868
- console.warn(
15869
- "[drainBulkQuotes] enrichQuoteRows failed; skipping batch",
15870
- {
15871
- underlying: resolvedUnderlying,
15872
- date,
15873
- rows: rows.length,
15874
- error: message
15875
- }
15876
- );
16029
+ console.warn("[drainBulkQuotes] enrichQuoteRows failed; skipping batch", {
16030
+ underlying: resolvedUnderlying,
16031
+ date,
16032
+ rows: rows.length,
16033
+ error: message
16034
+ });
15877
16035
  skipped.push({
15878
16036
  underlying: resolvedUnderlying,
15879
16037
  date,
@@ -15960,16 +16118,13 @@ var MarketIngestor = class {
15960
16118
  );
15961
16119
  } catch (error) {
15962
16120
  const message = error instanceof Error ? error.message : String(error);
15963
- console.warn(
15964
- "[writeQuotesForTicker] enrichQuoteRows failed; skipping batch",
15965
- {
15966
- underlying,
15967
- date,
15968
- ticker,
15969
- rows: rows.length,
15970
- error: message
15971
- }
15972
- );
16121
+ console.warn("[writeQuotesForTicker] enrichQuoteRows failed; skipping batch", {
16122
+ underlying,
16123
+ date,
16124
+ ticker,
16125
+ rows: rows.length,
16126
+ error: message
16127
+ });
15973
16128
  skipped.push({
15974
16129
  underlying,
15975
16130
  date,
@@ -16036,7 +16191,12 @@ var MarketIngestor = class {
16036
16191
  for (const underlying of opts.underlyings) {
16037
16192
  const upperUnderlying = underlying.toUpperCase();
16038
16193
  const assetClass = this.detectAssetClass(upperUnderlying);
16039
- const unsupported = this.preflightProviderSupport(provider, "chain", upperUnderlying, assetClass);
16194
+ const unsupported = this.preflightProviderSupport(
16195
+ provider,
16196
+ "chain",
16197
+ upperUnderlying,
16198
+ assetClass
16199
+ );
16040
16200
  if (unsupported) return unsupported;
16041
16201
  const dates = this.enumerateDates(opts.from, opts.to);
16042
16202
  for (const date of dates) {
@@ -16048,7 +16208,13 @@ var MarketIngestor = class {
16048
16208
  expired: true
16049
16209
  });
16050
16210
  } catch (error) {
16051
- const mapped = this.mapProviderFailure(provider, "chain", upperUnderlying, error, assetClass);
16211
+ const mapped = this.mapProviderFailure(
16212
+ provider,
16213
+ "chain",
16214
+ upperUnderlying,
16215
+ error,
16216
+ assetClass
16217
+ );
16052
16218
  if (mapped) return mapped;
16053
16219
  throw error;
16054
16220
  }
@@ -16196,43 +16362,75 @@ var MarketIngestor = class {
16196
16362
  case "spot_bars": {
16197
16363
  const ticker = opts.partition.ticker;
16198
16364
  if (!ticker) {
16199
- return { status: "error", rowsWritten: 0, error: "partition.ticker is required for datasetType='spot_bars'" };
16365
+ return {
16366
+ status: "error",
16367
+ rowsWritten: 0,
16368
+ error: "partition.ticker is required for datasetType='spot_bars'"
16369
+ };
16200
16370
  }
16201
16371
  const { rowCount } = await this.deps.stores.spot.writeFromSelect(
16202
16372
  { ticker: ticker.toUpperCase(), date: partitionDate },
16203
16373
  opts.selectSql
16204
16374
  );
16205
- return { status: "ok", rowsWritten: rowCount, dateRange: { from: partitionDate, to: partitionDate } };
16375
+ return {
16376
+ status: "ok",
16377
+ rowsWritten: rowCount,
16378
+ dateRange: { from: partitionDate, to: partitionDate }
16379
+ };
16206
16380
  }
16207
16381
  case "option_quotes": {
16208
16382
  const underlying = opts.partition.underlying;
16209
16383
  if (!underlying) {
16210
- return { status: "error", rowsWritten: 0, error: "partition.underlying is required for datasetType='option_quotes'" };
16384
+ return {
16385
+ status: "error",
16386
+ rowsWritten: 0,
16387
+ error: "partition.underlying is required for datasetType='option_quotes'"
16388
+ };
16211
16389
  }
16212
16390
  const { rowCount } = await this.deps.stores.quote.writeFromSelect(
16213
16391
  { underlying: underlying.toUpperCase(), date: partitionDate },
16214
16392
  opts.selectSql
16215
16393
  );
16216
- return { status: "ok", rowsWritten: rowCount, dateRange: { from: partitionDate, to: partitionDate } };
16394
+ return {
16395
+ status: "ok",
16396
+ rowsWritten: rowCount,
16397
+ dateRange: { from: partitionDate, to: partitionDate }
16398
+ };
16217
16399
  }
16218
16400
  case "option_chain": {
16219
16401
  const underlying = opts.partition.underlying;
16220
16402
  if (!underlying) {
16221
- return { status: "error", rowsWritten: 0, error: "partition.underlying is required for datasetType='option_chain'" };
16403
+ return {
16404
+ status: "error",
16405
+ rowsWritten: 0,
16406
+ error: "partition.underlying is required for datasetType='option_chain'"
16407
+ };
16222
16408
  }
16223
16409
  const { rowCount } = await this.deps.stores.chain.writeFromSelect(
16224
16410
  { underlying: underlying.toUpperCase(), date: partitionDate },
16225
16411
  opts.selectSql
16226
16412
  );
16227
- return { status: "ok", rowsWritten: rowCount, dateRange: { from: partitionDate, to: partitionDate } };
16413
+ return {
16414
+ status: "ok",
16415
+ rowsWritten: rowCount,
16416
+ dateRange: { from: partitionDate, to: partitionDate }
16417
+ };
16228
16418
  }
16229
16419
  default: {
16230
16420
  const _exhaustive = opts.datasetType;
16231
- return { status: "error", rowsWritten: 0, error: `Unknown datasetType: ${String(_exhaustive)}` };
16421
+ return {
16422
+ status: "error",
16423
+ rowsWritten: 0,
16424
+ error: `Unknown datasetType: ${String(_exhaustive)}`
16425
+ };
16232
16426
  }
16233
16427
  }
16234
16428
  } catch (err) {
16235
- return { status: "error", rowsWritten: 0, error: err instanceof Error ? err.message : String(err) };
16429
+ return {
16430
+ status: "error",
16431
+ rowsWritten: 0,
16432
+ error: err instanceof Error ? err.message : String(err)
16433
+ };
16236
16434
  }
16237
16435
  }
16238
16436
  async computeVixContext(opts) {
@@ -16322,7 +16520,11 @@ var MarketIngestor = class {
16322
16520
  const coverage = {};
16323
16521
  for (const ticker of opts.spotTickers) {
16324
16522
  try {
16325
- const cov = await this.deps.stores.spot.getCoverage(ticker.toUpperCase(), opts.asOf, opts.asOf);
16523
+ const cov = await this.deps.stores.spot.getCoverage(
16524
+ ticker.toUpperCase(),
16525
+ opts.asOf,
16526
+ opts.asOf
16527
+ );
16326
16528
  coverage[ticker] = {
16327
16529
  totalDates: cov.totalDates,
16328
16530
  dateRange: cov.earliest && cov.latest ? { from: cov.earliest, to: cov.latest } : void 0
@@ -16346,7 +16548,13 @@ var MarketIngestor = class {
16346
16548
  else status = "ok";
16347
16549
  return {
16348
16550
  status,
16349
- perOperation: { spot: spotResults, chain: chainResults, quotes: quoteResults, openInterest: openInterestResults, vixContext },
16551
+ perOperation: {
16552
+ spot: spotResults,
16553
+ chain: chainResults,
16554
+ quotes: quoteResults,
16555
+ openInterest: openInterestResults,
16556
+ vixContext
16557
+ },
16350
16558
  coverage,
16351
16559
  errors,
16352
16560
  ...aggregateSkipped.length > 0 ? { skipped: aggregateSkipped } : {}
@@ -16395,8 +16603,12 @@ function registerMarketFetchTools(server, baseDir, stores) {
16395
16603
  {
16396
16604
  description: "Fetch minute-level option quotes. Two modes \u2014 pass EITHER 'tickers' (specific OCC contracts, per-ticker provider calls) OR 'underlyings' (every contract under a symbol, provider bulk-by-root path \u2014 ThetaData only). ThetaData MDDS bulk mode enumerates contracts, fetches concrete quote history in bounded batches, and reuses first-order greeks bands per expiration/day with concrete-greeks fallback for misses. Writes to market.option_quote_minutes. The 'source' column tags Massive provider rows: 'nbbo' (real bid/ask via /v3/quotes when MASSIVE_DATA_TIER=quotes) or 'synth_close' (synthesized from /v2/aggs OHLCV close when the user's plan lacks /v3/quotes; bid=ask=close). ThetaData per-ticker rows are tagged 'nbbo'; ThetaData bulk rows are true NBBO and may persist source as NULL until the bulk row contract carries source. Massive falls back automatically on Developer/lower plans; ThetaData (both modes) returns true NBBO. Returns status='unsupported' with a clear error only when the active provider lacks the requested mode (e.g., bulk-by-underlying on Massive).",
16397
16605
  inputSchema: z29.object({
16398
- tickers: z29.array(z29.string()).min(1).optional().describe("Option tickers in OCC format (e.g., 'SPXW260321C05800000'). No O: prefix. Mutually exclusive with 'underlyings'."),
16399
- underlyings: z29.array(z29.string()).min(1).optional().describe("Underlying symbols (e.g. ['SPX']). Bulk mode \u2014 returns every contract's minute quotes per date. Mutually exclusive with 'tickers'."),
16606
+ tickers: z29.array(z29.string()).min(1).optional().describe(
16607
+ "Option tickers in OCC format (e.g., 'SPXW260321C05800000'). No O: prefix. Mutually exclusive with 'underlyings'."
16608
+ ),
16609
+ underlyings: z29.array(z29.string()).min(1).optional().describe(
16610
+ "Underlying symbols (e.g. ['SPX']). Bulk mode \u2014 returns every contract's minute quotes per date. Mutually exclusive with 'tickers'."
16611
+ ),
16400
16612
  from: z29.string().describe("Start date YYYY-MM-DD"),
16401
16613
  to: z29.string().describe("End date YYYY-MM-DD"),
16402
16614
  provider: z29.enum(["massive", "thetadata"]).optional(),
@@ -16455,7 +16667,13 @@ function registerMarketFetchTools(server, baseDir, stores) {
16455
16667
  async ({ underlyings, from, to, provider, dry_run }) => {
16456
16668
  await upgradeToReadWrite(baseDir);
16457
16669
  try {
16458
- const result = await ingestor.ingestChain({ underlyings, from, to, provider, dryRun: dry_run });
16670
+ const result = await ingestor.ingestChain({
16671
+ underlyings,
16672
+ from,
16673
+ to,
16674
+ provider,
16675
+ dryRun: dry_run
16676
+ });
16459
16677
  const summary = result.status === "unsupported" ? `Unsupported: ${result.error}` : dry_run ? `[DRY RUN] Would fetch chain for ${underlyings.join(", ")}` : `${result.status}: wrote ${result.rowsWritten} contract rows`;
16460
16678
  return createToolOutput(summary, result);
16461
16679
  } finally {
@@ -16542,7 +16760,15 @@ function registerMarketFetchTools(server, baseDir, stores) {
16542
16760
  provider: z29.enum(["massive", "thetadata"]).optional()
16543
16761
  })
16544
16762
  },
16545
- async ({ asOf, spot_tickers, chain_underlyings, quote_tickers, quote_underlyings, compute_vix_context, provider }, extra) => {
16763
+ async ({
16764
+ asOf,
16765
+ spot_tickers,
16766
+ chain_underlyings,
16767
+ quote_tickers,
16768
+ quote_underlyings,
16769
+ compute_vix_context,
16770
+ provider
16771
+ }, extra) => {
16546
16772
  await upgradeToReadWrite(baseDir);
16547
16773
  try {
16548
16774
  const progressToken = extra?._meta?.progressToken;
@@ -16577,7 +16803,9 @@ function registerMarketFetchTools(server, baseDir, stores) {
16577
16803
  ...result.perOperation.quotes,
16578
16804
  ...result.perOperation.vixContext ? [result.perOperation.vixContext] : []
16579
16805
  ];
16580
- const unsupportedCount = operationResults.filter((item) => item.status === "unsupported").length;
16806
+ const unsupportedCount = operationResults.filter(
16807
+ (item) => item.status === "unsupported"
16808
+ ).length;
16581
16809
  const skippedOpCount = operationResults.filter((item) => item.status === "skipped").length;
16582
16810
  const partialBatchCount = result.skipped?.length ?? 0;
16583
16811
  const partialSuffix = result.status === "partial" && partialBatchCount > 0 ? ` \u2014 ${partialBatchCount} batch(es) skipped on enrichment (see result.skipped[])` : "";
@@ -16604,18 +16832,14 @@ var UnderlyingsFileSchema = z30.object({
16604
16832
  });
16605
16833
  var registerUnderlyingSchema = z30.object({
16606
16834
  underlying: z30.string().min(1).max(16).regex(TICKER_RE).describe("Canonical underlying symbol, e.g. SPX"),
16607
- roots: z30.array(z30.string().min(1).max(16).regex(TICKER_RE)).min(1).max(32).describe(
16608
- "OCC roots that resolve to this underlying, e.g. ['SPX','SPXW','SPXQ']"
16609
- )
16835
+ roots: z30.array(z30.string().min(1).max(16).regex(TICKER_RE)).min(1).max(32).describe("OCC roots that resolve to this underlying, e.g. ['SPX','SPXW','SPXQ']")
16610
16836
  });
16611
16837
  var unregisterUnderlyingSchema = z30.object({
16612
16838
  underlying: z30.string().min(1).max(16).regex(TICKER_RE).describe("Underlying to remove. Bundled defaults cannot be removed.")
16613
16839
  });
16614
16840
  var listUnderlyingsSchema = z30.object({});
16615
16841
  var resolveRootSchema = z30.object({
16616
- input: z30.string().min(1).max(32).describe(
16617
- "Bare root ('SPXW') or full OCC ticker ('SPXW251219C05000000')"
16618
- )
16842
+ input: z30.string().min(1).max(32).describe("Bare root ('SPXW') or full OCC ticker ('SPXW251219C05000000')")
16619
16843
  });
16620
16844
 
16621
16845
  // src/market/tickers/loader.ts
@@ -16630,9 +16854,7 @@ function validate(underlying, roots) {
16630
16854
  }
16631
16855
  for (const r of roots) {
16632
16856
  if (!TICKER_RE.test(r)) {
16633
- throw new Error(
16634
- `TickerRegistry: invalid root "${r}" \u2014 must match ${TICKER_RE.source}`
16635
- );
16857
+ throw new Error(`TickerRegistry: invalid root "${r}" \u2014 must match ${TICKER_RE.source}`);
16636
16858
  }
16637
16859
  }
16638
16860
  }
@@ -16715,9 +16937,7 @@ var TickerRegistry = class {
16715
16937
  unregister(underlying) {
16716
16938
  const entry = this.entries.get(underlying);
16717
16939
  if (!entry) {
16718
- throw new Error(
16719
- `TickerRegistry.unregister: unknown underlying "${underlying}"`
16720
- );
16940
+ throw new Error(`TickerRegistry.unregister: unknown underlying "${underlying}"`);
16721
16941
  }
16722
16942
  if (entry.source === "default") {
16723
16943
  throw new Error(
@@ -17295,11 +17515,7 @@ var ParquetSpotStore = class extends SpotStore {
17295
17515
  }));
17296
17516
  }
17297
17517
  async getCoverage(ticker, from, to) {
17298
- const tickerDir = path7.join(
17299
- resolveMarketDir(this.ctx.dataDir),
17300
- "spot",
17301
- `ticker=${ticker}`
17302
- );
17518
+ const tickerDir = path7.join(resolveMarketDir(this.ctx.dataDir), "spot", `ticker=${ticker}`);
17303
17519
  if (!existsSync3(tickerDir)) {
17304
17520
  return { earliest: null, latest: null, missingDates: [], totalDates: 0 };
17305
17521
  }
@@ -17717,7 +17933,9 @@ async function setupParquetWorkingTables(conn, dataDir) {
17717
17933
  const enrichedTickerGlob = path8.join(enrichedDir, "ticker=*", "data.parquet");
17718
17934
  const enrichedContextPath = path8.join(enrichedDir, "context", "data.parquet");
17719
17935
  if (existsSync4(dailyPath)) {
17720
- await conn.run(`CREATE TEMP TABLE "${dailyTable}" AS SELECT * FROM read_parquet('${dailyPath}')`);
17936
+ await conn.run(
17937
+ `CREATE TEMP TABLE "${dailyTable}" AS SELECT * FROM read_parquet('${dailyPath}')`
17938
+ );
17721
17939
  await alignDailyWorkingTableColumns(conn, dailyTable);
17722
17940
  } else if (hasEnrichedTickerFiles(enrichedDir)) {
17723
17941
  await conn.run(
@@ -17732,9 +17950,7 @@ async function setupParquetWorkingTables(conn, dataDir) {
17732
17950
  }
17733
17951
  await alignDailyWorkingTableColumns(conn, dailyTable);
17734
17952
  } else {
17735
- await conn.run(
17736
- `CREATE TEMP TABLE "${dailyTable}" AS SELECT * FROM market.enriched WHERE 1=0`
17737
- );
17953
+ await conn.run(`CREATE TEMP TABLE "${dailyTable}" AS SELECT * FROM market.enriched WHERE 1=0`);
17738
17954
  for (const ohlcv of ["open", "high", "low", "close"]) {
17739
17955
  try {
17740
17956
  await conn.run(`ALTER TABLE "${dailyTable}" ADD COLUMN "${ohlcv}" DOUBLE`);
@@ -17757,7 +17973,9 @@ async function setupParquetWorkingTables(conn, dataDir) {
17757
17973
  } catch {
17758
17974
  }
17759
17975
  if (existsSync4(dateContextPath)) {
17760
- await conn.run(`CREATE TEMP TABLE "${dateContextTable}" AS SELECT * FROM read_parquet('${dateContextPath}')`);
17976
+ await conn.run(
17977
+ `CREATE TEMP TABLE "${dateContextTable}" AS SELECT * FROM read_parquet('${dateContextPath}')`
17978
+ );
17761
17979
  } else if (existsSync4(enrichedContextPath)) {
17762
17980
  await conn.run(
17763
17981
  `CREATE TEMP TABLE "${dateContextTable}" AS SELECT * FROM read_parquet('${enrichedContextPath}')`
@@ -17768,7 +17986,9 @@ async function setupParquetWorkingTables(conn, dataDir) {
17768
17986
  Trend_Direction VARCHAR, VIX_Spike_Pct DOUBLE, VIX_Gap_Pct DOUBLE
17769
17987
  )`);
17770
17988
  }
17771
- await conn.run(`CREATE UNIQUE INDEX "idx_${dateContextTable}_date" ON "${dateContextTable}"(date)`);
17989
+ await conn.run(
17990
+ `CREATE UNIQUE INDEX "idx_${dateContextTable}_date" ON "${dateContextTable}"(date)`
17991
+ );
17772
17992
  await conn.run(`CREATE UNIQUE INDEX "idx_${dailyTable}_pk" ON "${dailyTable}"(ticker, date)`);
17773
17993
  return { dailyTable, dateContextTable };
17774
17994
  }
@@ -17795,12 +18015,7 @@ async function flushEnrichedToParquet(conn, dataDir, ticker, tables) {
17795
18015
  targetPath: tickerFile,
17796
18016
  selectQuery: `SELECT ticker, date, ${enrichedColList} FROM "${tables.dailyTable}" WHERE ticker = '${ticker}' ORDER BY date`
17797
18017
  });
17798
- const contextFile = path8.join(
17799
- resolveMarketDir(dataDir),
17800
- "enriched",
17801
- "context",
17802
- "data.parquet"
17803
- );
18018
+ const contextFile = path8.join(resolveMarketDir(dataDir), "enriched", "context", "data.parquet");
17804
18019
  await writeParquetAtomic(conn, {
17805
18020
  targetPath: contextFile,
17806
18021
  selectQuery: `SELECT * FROM "${tables.dateContextTable}" ORDER BY date`
@@ -17836,7 +18051,9 @@ async function runTier2(conn, targets, spotStore) {
17836
18051
  const BATCH_SIZE = 500;
17837
18052
  for (let start = 0; start < bars.length; start += BATCH_SIZE) {
17838
18053
  const batch = bars.slice(start, start + BATCH_SIZE);
17839
- const placeholders = batch.map((_, i) => `($${i * 6 + 1},$${i * 6 + 2},$${i * 6 + 3},$${i * 6 + 4},$${i * 6 + 5},$${i * 6 + 6})`).join(",");
18054
+ const placeholders = batch.map(
18055
+ (_, i) => `($${i * 6 + 1},$${i * 6 + 2},$${i * 6 + 3},$${i * 6 + 4},$${i * 6 + 5},$${i * 6 + 6})`
18056
+ ).join(",");
17840
18057
  const params = batch.flatMap((b) => [b.ticker, b.date, b.open, b.high, b.low, b.close]);
17841
18058
  await conn.run(
17842
18059
  `INSERT INTO "${vixTempTable}" VALUES ${placeholders}`,
@@ -17968,12 +18185,21 @@ async function runTier2(conn, targets, spotStore) {
17968
18185
  };
17969
18186
  });
17970
18187
  const enrichedContext = computeVIXDerivedFields(contextRows);
17971
- const derivedCols = ["date", "Vol_Regime", "Term_Structure_State", "Trend_Direction", "VIX_Spike_Pct", "VIX_Gap_Pct"];
18188
+ const derivedCols = [
18189
+ "date",
18190
+ "Vol_Regime",
18191
+ "Term_Structure_State",
18192
+ "Trend_Direction",
18193
+ "VIX_Spike_Pct",
18194
+ "VIX_Gap_Pct"
18195
+ ];
17972
18196
  const BATCH_SIZE = 500;
17973
18197
  for (let start = 0; start < enrichedContext.length; start += BATCH_SIZE) {
17974
18198
  const batch = enrichedContext.slice(start, start + BATCH_SIZE);
17975
18199
  const placeholders = batch.map((_, rowIdx) => {
17976
- const params2 = derivedCols.map((__, colIdx) => `$${rowIdx * derivedCols.length + colIdx + 1}`);
18200
+ const params2 = derivedCols.map(
18201
+ (__, colIdx) => `$${rowIdx * derivedCols.length + colIdx + 1}`
18202
+ );
17977
18203
  return `(${params2.join(", ")})`;
17978
18204
  }).join(", ");
17979
18205
  const sql = `INSERT OR REPLACE INTO ${dateContextTarget} (${derivedCols.join(", ")}) VALUES ${placeholders}`;
@@ -18007,10 +18233,9 @@ async function hasTier3Data(conn, ticker, spotStore) {
18007
18233
  const cov = await spotStore.getCoverage(ticker, "1970-01-01", "9999-12-31");
18008
18234
  return cov.totalDates > 0;
18009
18235
  }
18010
- const r = await conn.runAndReadAll(
18011
- `SELECT COUNT(*) FROM market.spot WHERE ticker = $1 LIMIT 1`,
18012
- [ticker]
18013
- );
18236
+ const r = await conn.runAndReadAll(`SELECT COUNT(*) FROM market.spot WHERE ticker = $1 LIMIT 1`, [
18237
+ ticker
18238
+ ]);
18014
18239
  return Number(r.getRows()[0]?.[0] ?? 0) > 0;
18015
18240
  }
18016
18241
  async function runEnrichment(conn, ticker, opts = {}, io) {
@@ -18047,7 +18272,10 @@ async function runEnrichment(conn, ticker, opts = {}, io) {
18047
18272
  fetchParams.push(lookbackStart);
18048
18273
  }
18049
18274
  fetchSql += ` ORDER BY date ASC`;
18050
- const rawReader = await conn.runAndReadAll(fetchSql, fetchParams);
18275
+ const rawReader = await conn.runAndReadAll(
18276
+ fetchSql,
18277
+ fetchParams
18278
+ );
18051
18279
  rawRows = rawReader.getRows();
18052
18280
  }
18053
18281
  if (rawRows.length === 0) {
@@ -18264,7 +18492,8 @@ function computeIntradayTimingFields(bars) {
18264
18492
  const highInAfternoon = highTime >= 12;
18265
18493
  const lowInAfternoon = lowTime >= 12;
18266
18494
  let reversalType = 0;
18267
- if (highInMorning && lowInAfternoon) reversalType = 1;
18495
+ if (highInMorning && lowInAfternoon)
18496
+ reversalType = 1;
18268
18497
  else if (lowInMorning && highInAfternoon) reversalType = -1;
18269
18498
  const openingBars = bars.filter((b) => hhmmToDecimalHours(b.time) < 10);
18270
18499
  let openingDriveStrength = 0;
@@ -18289,7 +18518,14 @@ function computeIntradayTimingFields(bars) {
18289
18518
  intradayRealizedVol = barStdDev * Math.sqrt(bars.length * 252);
18290
18519
  }
18291
18520
  }
18292
- return { highTime, lowTime, highBeforeLow, reversalType, openingDriveStrength, intradayRealizedVol };
18521
+ return {
18522
+ highTime,
18523
+ lowTime,
18524
+ highBeforeLow,
18525
+ reversalType,
18526
+ openingDriveStrength,
18527
+ intradayRealizedVol
18528
+ };
18293
18529
  }
18294
18530
  async function runTier3(conn, ticker, dates, dailyTarget = "market.enriched", spotStore) {
18295
18531
  const hasData = await hasTier3Data(conn, ticker, spotStore);
@@ -18355,7 +18591,14 @@ async function runTier3(conn, ticker, dates, dailyTarget = "market.enriched", sp
18355
18591
  reason: "intraday data exists but no bars overlap with enrichment date range"
18356
18592
  };
18357
18593
  }
18358
- const tier3Cols = ["High_Time", "Low_Time", "High_Before_Low", "Reversal_Type", "Opening_Drive_Strength", "Intraday_Realized_Vol"];
18594
+ const tier3Cols = [
18595
+ "High_Time",
18596
+ "Low_Time",
18597
+ "High_Before_Low",
18598
+ "Reversal_Type",
18599
+ "Opening_Drive_Strength",
18600
+ "Intraday_Realized_Vol"
18601
+ ];
18359
18602
  const enrichedRows = [];
18360
18603
  for (const [dateStr, bars] of barsByDate) {
18361
18604
  const timing = computeIntradayTimingFields(bars);
@@ -18446,9 +18689,7 @@ var ParquetEnrichedStore = class extends EnrichedStore {
18446
18689
  });
18447
18690
  const reader = await this.ctx.conn.runAndReadAll(sql);
18448
18691
  const names = reader.columnNames();
18449
- return reader.getRows().map(
18450
- (row) => Object.fromEntries(names.map((n, i) => [n, row[i]]))
18451
- );
18692
+ return reader.getRows().map((row) => Object.fromEntries(names.map((n, i) => [n, row[i]])));
18452
18693
  }
18453
18694
  async getCoverage(ticker) {
18454
18695
  const filePath = path9.join(
@@ -18521,9 +18762,7 @@ var DuckdbEnrichedStore = class extends EnrichedStore {
18521
18762
  });
18522
18763
  const reader = await this.ctx.conn.runAndReadAll(sql);
18523
18764
  const names = reader.columnNames();
18524
- return reader.getRows().map(
18525
- (row) => Object.fromEntries(names.map((n, i) => [n, row[i]]))
18526
- );
18765
+ return reader.getRows().map((row) => Object.fromEntries(names.map((n, i) => [n, row[i]])));
18527
18766
  }
18528
18767
  async getCoverage(ticker) {
18529
18768
  const tickerLit = ticker.replace(/'/g, "''");
@@ -18876,9 +19115,7 @@ var ParquetQuoteStore = class extends QuoteStore {
18876
19115
  }
18877
19116
  async readQuotes(occTickers, from, to) {
18878
19117
  if (occTickers.length === 0) return /* @__PURE__ */ new Map();
18879
- const firstUnderlying = this.ctx.tickers.resolve(
18880
- extractRoot(occTickers[0])
18881
- );
19118
+ const firstUnderlying = this.ctx.tickers.resolve(extractRoot(occTickers[0]));
18882
19119
  for (const t of occTickers) {
18883
19120
  const u = this.ctx.tickers.resolve(extractRoot(t));
18884
19121
  if (u !== firstUnderlying) {
@@ -19193,9 +19430,7 @@ var DuckdbQuoteStore = class extends QuoteStore {
19193
19430
  }
19194
19431
  async readQuotes(occTickers, from, to) {
19195
19432
  if (occTickers.length === 0) return /* @__PURE__ */ new Map();
19196
- const firstUnderlying = this.ctx.tickers.resolve(
19197
- extractRoot(occTickers[0])
19198
- );
19433
+ const firstUnderlying = this.ctx.tickers.resolve(extractRoot(occTickers[0]));
19199
19434
  for (const t of occTickers) {
19200
19435
  const u = this.ctx.tickers.resolve(extractRoot(t));
19201
19436
  if (u !== firstUnderlying) {
@@ -19646,7 +19881,7 @@ async function startTradeBlocksMcp(options = {}) {
19646
19881
  console.error(`Error: Blocks directory does not exist: ${resolvedBlocksDir}`);
19647
19882
  process.exit(1);
19648
19883
  }
19649
- const { setBlocksDir } = await import("./sync-QFI5L7S7.js");
19884
+ const { setBlocksDir } = await import("./sync-V25UQJA3.js");
19650
19885
  setBlocksDir(resolvedBlocksDir);
19651
19886
  }
19652
19887
  const tickerRegistry = await loadRegistry({ dataDir: resolvedDir });
@@ -19668,9 +19903,7 @@ async function startTradeBlocksMcp(options = {}) {
19668
19903
  parquetMode,
19669
19904
  getCurrentConnection
19670
19905
  };
19671
- console.error(
19672
- `[market-stores] Constructed: ${parquetMode ? "parquet" : "duckdb"} backend`
19673
- );
19906
+ console.error(`[market-stores] Constructed: ${parquetMode ? "parquet" : "duckdb"} backend`);
19674
19907
  const createServer = () => {
19675
19908
  const server = new McpServer(
19676
19909
  { name: "tradeblocks-mcp", version: "2.0.0" },
@@ -19686,7 +19919,7 @@ async function startTradeBlocksMcp(options = {}) {
19686
19919
  return server;
19687
19920
  };
19688
19921
  if (http) {
19689
- const { loadAuthConfig } = await import("./config-6IZXEFEX.js");
19922
+ const { loadAuthConfig } = await import("./config-DK7KOMNL.js");
19690
19923
  let auth;
19691
19924
  try {
19692
19925
  auth = loadAuthConfig({ noAuth });