tradeblocks-mcp 3.0.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +85 -80
  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-QTTR7AAW.js → chunk-2I3S2ZLT.js} +325 -84
  7. package/dist/chunk-2I3S2ZLT.js.map +1 -0
  8. package/dist/{chunk-4BLCXNQ6.js → chunk-4HZLVUOZ.js} +179 -183
  9. package/dist/chunk-4HZLVUOZ.js.map +1 -0
  10. package/dist/{chunk-XXYOUIZY.js → chunk-NWNSKALN.js} +63 -80
  11. package/dist/chunk-NWNSKALN.js.map +1 -0
  12. package/dist/{chunk-PRAYH3RT.js → chunk-WNTYA7ER.js} +29 -12
  13. package/dist/chunk-WNTYA7ER.js.map +1 -0
  14. package/dist/{chunk-BKQ4PM4Y.js → chunk-YZA4RS76.js} +24 -18
  15. package/dist/chunk-YZA4RS76.js.map +1 -0
  16. package/dist/{chunk-W2PP3LEH.js → chunk-ZTJXC3LS.js} +17 -8
  17. package/dist/chunk-ZTJXC3LS.js.map +1 -0
  18. package/dist/daily-log-processor-MZW2XBZY.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-6U33TQUT.js +16 -0
  22. package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
  23. package/dist/{sync-PO4IPCYV.js → sync-3UBC37VW.js} +7 -7
  24. package/dist/test-exports.js +876 -483
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-DKDCWGKD.js +9 -0
  27. package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
  28. package/package.json +2 -6
  29. package/server/{chunk-CSDVJPBB.js → chunk-536NSFED.js} +29 -12
  30. package/server/chunk-536NSFED.js.map +1 -0
  31. package/server/{chunk-WA5AAPCH.js → chunk-A3UKD64A.js} +17 -8
  32. package/server/chunk-A3UKD64A.js.map +1 -0
  33. package/server/{chunk-PNKG7RY7.js → chunk-OMUDNJBJ.js} +63 -80
  34. package/server/chunk-OMUDNJBJ.js.map +1 -0
  35. package/server/{chunk-72GKJE2U.js → chunk-RJDJHZ5Y.js} +934 -335
  36. package/server/chunk-RJDJHZ5Y.js.map +1 -0
  37. package/server/{chunk-NZO6PT64.js → chunk-RM4B2VKS.js} +167 -177
  38. package/server/chunk-RM4B2VKS.js.map +1 -0
  39. package/server/{chunk-5EBXHT6C.js → chunk-SY6GDNVI.js} +24 -18
  40. package/server/chunk-SY6GDNVI.js.map +1 -0
  41. package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
  42. package/server/chunk-T66KH2XH.js.map +1 -0
  43. package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
  44. package/server/chunk-W5E7FHC4.js.map +1 -0
  45. package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
  46. package/server/config-DK7KOMNL.js.map +1 -0
  47. package/server/daily-log-processor-B2VPU2FQ.js +10 -0
  48. package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
  49. package/server/http-server.js +2 -5
  50. package/server/index.js +2497 -2029
  51. package/server/index.js.map +1 -1
  52. package/server/iv-solver-worker.js +9 -1
  53. package/server/market-provider-L56RFHAC.js +17 -0
  54. package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
  55. package/server/{sync-QFI5L7S7.js → sync-CENKDN53.js} +7 -7
  56. package/server/trade-processor-3PI4LHZI.js +10 -0
  57. package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
  58. package/src/auth/clients-store.ts +4 -4
  59. package/src/auth/code-store.ts +1 -1
  60. package/src/auth/config.ts +11 -12
  61. package/src/auth/login-page.ts +8 -10
  62. package/src/auth/provider.ts +35 -30
  63. package/src/auth/token.ts +17 -15
  64. package/src/db/backtest-schemas.ts +38 -8
  65. package/src/db/connection.ts +107 -40
  66. package/src/db/data-root.ts +22 -0
  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 +139 -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/index.ts +2 -0
  114. package/src/tools/blocks/paired-comparison.ts +425 -0
  115. package/src/tools/blocks/similarity.ts +316 -283
  116. package/src/tools/edge-decay.ts +124 -197
  117. package/src/tools/exit-analysis.ts +130 -77
  118. package/src/tools/greeks-attribution.ts +84 -35
  119. package/src/tools/guides.ts +4 -6
  120. package/src/tools/imports.ts +11 -14
  121. package/src/tools/market-data.ts +1067 -757
  122. package/src/tools/market-enrichment.ts +3 -3
  123. package/src/tools/market-fetch.ts +148 -67
  124. package/src/tools/market-imports.ts +12 -12
  125. package/src/tools/middleware/sync-middleware.ts +5 -6
  126. package/src/tools/performance.ts +185 -302
  127. package/src/tools/profile-analysis.ts +52 -66
  128. package/src/tools/profiles.ts +106 -69
  129. package/src/tools/regime-advisor.ts +20 -45
  130. package/src/tools/replay.ts +81 -77
  131. package/src/tools/reports/discrepancies.ts +298 -328
  132. package/src/tools/reports/fields.ts +7 -25
  133. package/src/tools/reports/helpers.ts +18 -49
  134. package/src/tools/reports/predictive.ts +27 -70
  135. package/src/tools/reports/slippage-trends.ts +315 -345
  136. package/src/tools/reports/slippage.ts +1 -4
  137. package/src/tools/reports/strategy-matches.ts +399 -441
  138. package/src/tools/schema.ts +43 -40
  139. package/src/tools/shared/filters.ts +3 -9
  140. package/src/tools/snapshot.ts +9 -30
  141. package/src/tools/sql.ts +15 -14
  142. package/src/tools/tickers.ts +1 -4
  143. package/src/utils/batch-exit-analysis.ts +31 -42
  144. package/src/utils/black-scholes.ts +39 -29
  145. package/src/utils/block-loader.ts +69 -83
  146. package/src/utils/calibration-probe.ts +3 -4
  147. package/src/utils/chain-loader.ts +3 -3
  148. package/src/utils/csv-discovery.ts +16 -22
  149. package/src/utils/data-quality.ts +24 -36
  150. package/src/utils/exit-triggers.ts +91 -96
  151. package/src/utils/field-timing.ts +94 -79
  152. package/src/utils/filter-predicates.ts +13 -9
  153. package/src/utils/flatfile-importer.ts +94 -64
  154. package/src/utils/greeks-decomposition.ts +152 -100
  155. package/src/utils/iv-solver-pool.ts +55 -25
  156. package/src/utils/iv-solver-worker.ts +5 -5
  157. package/src/utils/market-enricher.ts +528 -497
  158. package/src/utils/market-importer.ts +31 -12
  159. package/src/utils/market-provider.ts +21 -23
  160. package/src/utils/massive-tier.ts +5 -7
  161. package/src/utils/migrate-option-data-helpers.ts +2 -8
  162. package/src/utils/option-quote-greeks.ts +25 -31
  163. package/src/utils/option-time.ts +4 -8
  164. package/src/utils/output-formatter.ts +1 -4
  165. package/src/utils/provider-capabilities.ts +1 -4
  166. package/src/utils/providers/massive.ts +59 -93
  167. package/src/utils/providers/thetadata/backfill.ts +14 -23
  168. package/src/utils/providers/thetadata/client.ts +12 -8
  169. package/src/utils/providers/thetadata/decode.ts +2 -20
  170. package/src/utils/providers/thetadata/endpoints.ts +32 -40
  171. package/src/utils/providers/thetadata/join.ts +11 -10
  172. package/src/utils/providers/thetadata/proto.ts +12 -10
  173. package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
  174. package/src/utils/providers/thetadata.ts +11 -10
  175. package/src/utils/quote-enricher.ts +4 -4
  176. package/src/utils/quote-parquet-projection.ts +3 -11
  177. package/src/utils/sample-date-selector.ts +3 -5
  178. package/src/utils/schema-metadata.ts +102 -70
  179. package/src/utils/ticker.ts +5 -9
  180. package/src/utils/trade-replay.ts +77 -68
  181. package/dist/chunk-4BLCXNQ6.js.map +0 -1
  182. package/dist/chunk-BKQ4PM4Y.js.map +0 -1
  183. package/dist/chunk-FGZH632F.js.map +0 -1
  184. package/dist/chunk-LDKTV7GW.js.map +0 -1
  185. package/dist/chunk-PRAYH3RT.js.map +0 -1
  186. package/dist/chunk-QTTR7AAW.js.map +0 -1
  187. package/dist/chunk-W2PP3LEH.js.map +0 -1
  188. package/dist/chunk-XXYOUIZY.js.map +0 -1
  189. package/dist/daily-log-processor-6MWJ23JK.js +0 -9
  190. package/dist/market-provider-VDRJUEF2.js +0 -16
  191. package/dist/trade-processor-NHU4VWRX.js +0 -9
  192. package/manifest.json +0 -141
  193. package/server/chunk-5EBXHT6C.js.map +0 -1
  194. package/server/chunk-72GKJE2U.js.map +0 -1
  195. package/server/chunk-CSDVJPBB.js.map +0 -1
  196. package/server/chunk-FBNDMCT5.js.map +0 -1
  197. package/server/chunk-NRFXAJF7.js.map +0 -1
  198. package/server/chunk-NZO6PT64.js.map +0 -1
  199. package/server/chunk-PNKG7RY7.js.map +0 -1
  200. package/server/chunk-WA5AAPCH.js.map +0 -1
  201. package/server/config-6IZXEFEX.js.map +0 -1
  202. package/server/daily-log-processor-Y3PVSVBM.js +0 -10
  203. package/server/market-provider-VOYYVYWT.js +0 -17
  204. package/server/trade-processor-JWVS37KM.js +0 -10
  205. /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-MZW2XBZY.js.map} +0 -0
  206. /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
  207. /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-6U33TQUT.js.map} +0 -0
  208. /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
  209. /package/dist/{sync-PO4IPCYV.js.map → sync-3UBC37VW.js.map} +0 -0
  210. /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-DKDCWGKD.js.map} +0 -0
  211. /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
  212. /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-B2VPU2FQ.js.map} +0 -0
  213. /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
  214. /package/server/{market-provider-VOYYVYWT.js.map → market-provider-L56RFHAC.js.map} +0 -0
  215. /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
  216. /package/server/{sync-QFI5L7S7.js.map → sync-CENKDN53.js.map} +0 -0
  217. /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-3PI4LHZI.js.map} +0 -0
  218. /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
@@ -10,6 +10,7 @@ import { registerComparisonBlockTools } from "./comparison.ts";
10
10
  import { registerAnalysisBlockTools } from "./analysis.ts";
11
11
  import { registerSimilarityBlockTools } from "./similarity.ts";
12
12
  import { registerHealthBlockTools } from "./health.ts";
13
+ import { registerPairedComparisonTool } from "./paired-comparison.ts";
13
14
 
14
15
  /**
15
16
  * Register all block-related MCP tools
@@ -20,4 +21,5 @@ export function registerBlockTools(server: McpServer, baseDir: string): void {
20
21
  registerAnalysisBlockTools(server, baseDir);
21
22
  registerSimilarityBlockTools(server, baseDir);
22
23
  registerHealthBlockTools(server, baseDir);
24
+ registerPairedComparisonTool(server, baseDir);
23
25
  }
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Paired Bootstrap Comparison Tool
3
+ *
4
+ * Honest confidence intervals for "is strategy A actually different from strategy
5
+ * B (or from zero)". Wraps the paired day-block bootstrap primitive from
6
+ * @tradeblocks/lib with a declarative attribution of each trade's P&L onto a
7
+ * daily grid derived from the block's own trade data.
8
+ */
9
+
10
+ import { z } from "zod";
11
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { loadBlock } from "../../utils/block-loader.ts";
13
+ import { createToolOutput, formatCurrency } from "../../utils/output-formatter.ts";
14
+ import {
15
+ pairedBlockBootstrap,
16
+ holdingPeriodBlockDays,
17
+ type DaySeries,
18
+ type PairedBlockBootstrapResult,
19
+ } from "@tradeblocks/lib";
20
+ import type { Trade } from "@tradeblocks/lib";
21
+ import { filterByStrategy, filterByDateRange } from "../shared/filters.ts";
22
+ import { withSyncedBlock } from "../middleware/sync-middleware.ts";
23
+
24
+ /**
25
+ * How each trade's P&L reaches the daily grid. Reused in the tool description
26
+ * and echoed in the structured output so the methodology is never implicit.
27
+ */
28
+ const ATTRIBUTION_NOTE =
29
+ "A trade's P&L is attributed evenly across the trading days it was open " +
30
+ "(dateOpened through dateClosed). Trading days are derived from the block's own " +
31
+ "trade data (the union of every trade's open and close dates) -- no market " +
32
+ "calendar is assumed, so weekends or holidays appear only if a trade spans them.";
33
+
34
+ // Frozen, mathematically-neutral defaults (documented on the schema). Block
35
+ // length is NEVER defaulted to a constant -- it is derived from the submitted
36
+ // block's own holding-period distribution unless the caller overrides it.
37
+ const DEFAULT_CI_LEVEL = 0.95;
38
+ const DEFAULT_RESAMPLES = 2000;
39
+ const DEFAULT_SEED = 42;
40
+ const SENSITIVITY_MULTIPLIERS = [0.5, 2];
41
+
42
+ export const pairedComparisonInputSchema = z.object({
43
+ blockId: z.string().describe("Block folder name to analyze"),
44
+ strategyA: z.string().describe("Strategy name for arm A (case-insensitive)"),
45
+ strategyB: z
46
+ .string()
47
+ .optional()
48
+ .describe(
49
+ "Strategy name for arm B (case-insensitive). If omitted, arm A is compared against a " +
50
+ "constant 0 -- i.e. is the edge distinguishable from nothing.",
51
+ ),
52
+ statistic: z
53
+ .enum(["mean_daily_pnl", "median_daily_pnl"])
54
+ .default("mean_daily_pnl")
55
+ .describe("Which daily-P&L functional to compare. Default mean_daily_pnl."),
56
+ dateRange: z
57
+ .object({
58
+ start: z.string().describe("Start date (YYYY-MM-DD)"),
59
+ end: z.string().describe("End date (YYYY-MM-DD)"),
60
+ })
61
+ .optional()
62
+ .describe("Optional date range filter applied to both arms (by trade open date)."),
63
+ ciLevel: z
64
+ .number()
65
+ .default(DEFAULT_CI_LEVEL)
66
+ .describe("Confidence level in (0, 1). Default 0.95 (deterministic)."),
67
+ resamples: z
68
+ .number()
69
+ .int()
70
+ .default(DEFAULT_RESAMPLES)
71
+ .describe("Number of bootstrap resamples. Default 2000 (deterministic)."),
72
+ seed: z
73
+ .number()
74
+ .int()
75
+ .default(DEFAULT_SEED)
76
+ .describe("PRNG seed. Same inputs + seed -> identical result. Default 42 (deterministic)."),
77
+ blockDays: z
78
+ .number()
79
+ .int()
80
+ .min(1)
81
+ .optional()
82
+ .describe(
83
+ "Override the block length. By default it is derived from this block's own " +
84
+ "holding-period distribution (95th percentile of days-open).",
85
+ ),
86
+ effectiveNFloorBlocks: z
87
+ .number()
88
+ .min(0)
89
+ .optional()
90
+ .describe(
91
+ "Optional: refuse with notComparable when the shared overlap yields fewer distinct " +
92
+ "blocks than this floor. When omitted, only structural degeneracy refuses (underpowered).",
93
+ ),
94
+ });
95
+
96
+ type PairedComparisonInput = z.infer<typeof pairedComparisonInputSchema>;
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Daily attribution
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Calendar date (YYYY-MM-DD) for a trade timestamp.
104
+ *
105
+ * Calendar dates are derived in UTC from the stored trade timestamps; the grid,
106
+ * the lib tests, and the displayed overlap windows all share this convention,
107
+ * so the result is independent of the host machine's timezone.
108
+ */
109
+ function toCalendarDateStr(date: Date): string {
110
+ const y = date.getUTCFullYear();
111
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
112
+ const d = String(date.getUTCDate()).padStart(2, "0");
113
+ return `${y}-${m}-${d}`;
114
+ }
115
+
116
+ /**
117
+ * The block's trading-day grid: the sorted, unique union of every trade's open
118
+ * and close calendar dates. Days strictly between an open and a close that are
119
+ * not themselves any trade's open/close are unknown to be trading days and are
120
+ * deliberately not invented.
121
+ */
122
+ export function buildBlockTradingDayIndex(trades: Trade[]): string[] {
123
+ const days = new Set<string>();
124
+ for (const t of trades) {
125
+ days.add(toCalendarDateStr(t.dateOpened));
126
+ days.add(toCalendarDateStr(t.dateClosed ?? t.dateOpened));
127
+ }
128
+ return Array.from(days).sort();
129
+ }
130
+
131
+ /** Inclusive index range [lo, hi] of grid days within [openStr, closeStr]. */
132
+ function gridSpan(grid: string[], openStr: string, closeStr: string): [number, number] {
133
+ let lo = grid.findIndex((d) => d >= openStr);
134
+ if (lo === -1) lo = grid.length;
135
+ let hi = -1;
136
+ for (let i = grid.length - 1; i >= 0; i--) {
137
+ if (grid[i] <= closeStr) {
138
+ hi = i;
139
+ break;
140
+ }
141
+ }
142
+ return [lo, hi];
143
+ }
144
+
145
+ /**
146
+ * Number of grid trading days each trade was open. This is the holding period
147
+ * used both for even P&L attribution and for block-length derivation.
148
+ */
149
+ export function armHoldingPeriods(armTrades: Trade[], grid: string[]): number[] {
150
+ return armTrades.map((t) => {
151
+ const openStr = toCalendarDateStr(t.dateOpened);
152
+ const closeStr = toCalendarDateStr(t.dateClosed ?? t.dateOpened);
153
+ const [lo, hi] = gridSpan(grid, openStr, closeStr);
154
+ return Math.max(1, hi - lo + 1);
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Build one arm's DaySeries on the shared trading-day grid. Each trade's P&L is
160
+ * spread evenly across the grid days it was open; a grid day the arm did not
161
+ * hold a position is not-observed (mask false), while a held day with a net-zero
162
+ * attributed P&L is a genuine observation (mask true).
163
+ */
164
+ export function buildArmDaySeries(armTrades: Trade[], grid: string[]): DaySeries {
165
+ const values = new Array<number>(grid.length).fill(0);
166
+ const observedMask = new Array<boolean>(grid.length).fill(false);
167
+
168
+ for (const t of armTrades) {
169
+ const openStr = toCalendarDateStr(t.dateOpened);
170
+ const closeStr = toCalendarDateStr(t.dateClosed ?? t.dateOpened);
171
+ const [lo, hi] = gridSpan(grid, openStr, closeStr);
172
+ if (hi < lo) continue;
173
+ const span = hi - lo + 1;
174
+ const perDay = t.pl / span;
175
+ for (let i = lo; i <= hi; i++) {
176
+ values[i] += perDay;
177
+ observedMask[i] = true;
178
+ }
179
+ }
180
+
181
+ return { index: grid, values, observedMask };
182
+ }
183
+
184
+ function countObserved(series: DaySeries): number {
185
+ return series.observedMask.reduce((n, o) => n + (o ? 1 : 0), 0);
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Statistics
190
+ // ---------------------------------------------------------------------------
191
+
192
+ function meanOf(values: number[]): number {
193
+ return values.reduce((s, v) => s + v, 0) / values.length;
194
+ }
195
+
196
+ function medianOf(values: number[]): number {
197
+ const sorted = [...values].sort((a, b) => a - b);
198
+ const mid = Math.floor(sorted.length / 2);
199
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
200
+ }
201
+
202
+ const STATISTIC_FNS: Record<PairedComparisonInput["statistic"], (d: number[]) => number> = {
203
+ mean_daily_pnl: meanOf,
204
+ median_daily_pnl: medianOf,
205
+ };
206
+
207
+ const STATISTIC_LABELS: Record<PairedComparisonInput["statistic"], string> = {
208
+ mean_daily_pnl: "mean daily P&L",
209
+ median_daily_pnl: "median daily P&L",
210
+ };
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Core computation (DB-free, directly testable)
214
+ // ---------------------------------------------------------------------------
215
+
216
+ export interface PairedComparisonParams {
217
+ armATrades: Trade[];
218
+ /** null -> compare arm A against a constant 0. */
219
+ armBTrades: Trade[] | null;
220
+ /** All block trades -- defines the shared trading-day grid. */
221
+ blockTrades: Trade[];
222
+ statistic: PairedComparisonInput["statistic"];
223
+ ciLevel: number;
224
+ resamples: number;
225
+ seed: number;
226
+ blockDays?: number;
227
+ /** Optional power floor -> notComparable when effectiveN falls below it. */
228
+ effectiveNFloorBlocks?: number;
229
+ strategyA: string;
230
+ strategyB?: string;
231
+ }
232
+
233
+ export interface PairedComparisonReport {
234
+ summary: string;
235
+ data: Record<string, unknown>;
236
+ result: PairedBlockBootstrapResult;
237
+ }
238
+
239
+ /**
240
+ * Run the paired day-block bootstrap for a strategy comparison and format an
241
+ * honest, plain-English report. Throws a clear error when a strategy filter
242
+ * matched zero trades.
243
+ */
244
+ export function runPairedBootstrapComparison(
245
+ params: PairedComparisonParams,
246
+ ): PairedComparisonReport {
247
+ const {
248
+ armATrades,
249
+ armBTrades,
250
+ blockTrades,
251
+ statistic,
252
+ ciLevel,
253
+ resamples,
254
+ seed,
255
+ blockDays: blockDaysOverride,
256
+ effectiveNFloorBlocks,
257
+ strategyA,
258
+ strategyB,
259
+ } = params;
260
+
261
+ if (armATrades.length === 0) {
262
+ throw new Error(`No trades found for strategy "${strategyA}".`);
263
+ }
264
+ if (armBTrades !== null && armBTrades.length === 0) {
265
+ throw new Error(`No trades found for strategy "${strategyB}".`);
266
+ }
267
+
268
+ const grid = buildBlockTradingDayIndex(blockTrades);
269
+ const armA = buildArmDaySeries(armATrades, grid);
270
+ const armB = armBTrades !== null ? buildArmDaySeries(armBTrades, grid) : null;
271
+
272
+ // Block length: derived from the LONGER arm's 95th-percentile holding period
273
+ // (the conservative choice), unless the caller overrides it.
274
+ const hpA = holdingPeriodBlockDays(armHoldingPeriods(armATrades, grid));
275
+ const hpB =
276
+ armBTrades !== null ? holdingPeriodBlockDays(armHoldingPeriods(armBTrades, grid)) : hpA;
277
+ const derivedBlockDays = Math.max(hpA, hpB);
278
+ const derivedFromArm = hpB > hpA ? "B" : "A";
279
+ const blockDays = blockDaysOverride ?? derivedBlockDays;
280
+
281
+ const blockDaysNote =
282
+ blockDaysOverride !== undefined
283
+ ? `block length = ${blockDays} trading day(s), caller-supplied override`
284
+ : armB !== null
285
+ ? `block length = ${blockDays} trading day(s), derived from the 95th-percentile ` +
286
+ `holding period of arm ${derivedFromArm} (the longer-held arm -- the conservative choice)`
287
+ : `block length = ${blockDays} trading day(s), derived from the 95th-percentile ` +
288
+ `holding period of arm A`;
289
+
290
+ const result = pairedBlockBootstrap({
291
+ armA,
292
+ armB: armB ?? { constant: 0 },
293
+ statistic: STATISTIC_FNS[statistic],
294
+ holdingRule: { blockDays, sensitivity: SENSITIVITY_MULTIPLIERS },
295
+ ciLevel,
296
+ resamples,
297
+ seed,
298
+ effectiveNFloorBlocks,
299
+ });
300
+
301
+ const statLabel = STATISTIC_LABELS[statistic];
302
+ const comparisonLabel = armB !== null ? `${strategyA} minus ${strategyB}` : `${strategyA} vs 0`;
303
+
304
+ // Plain-English lead line -- refusal is surfaced first, never buried.
305
+ let summary: string;
306
+ let refusalMessage: string | null = null;
307
+ if (result.status === "notComparable") {
308
+ refusalMessage =
309
+ `Not comparable: effective sample (${result.effectiveN.toFixed(2)} blocks) is below the ` +
310
+ `required power floor. A confidence interval is withheld.`;
311
+ summary = `Paired comparison (${comparisonLabel}): ${refusalMessage}`;
312
+ } else if (result.status === "underpowered") {
313
+ refusalMessage =
314
+ `Underpowered: the shared overlap is too short to resample at a block length of ` +
315
+ `${blockDays} trading day(s) (fewer than two drawable blocks). A confidence interval is withheld.`;
316
+ summary = `Paired comparison (${comparisonLabel}): ${refusalMessage}`;
317
+ } else {
318
+ const point = result.point ?? 0;
319
+ const low = result.ci.low ?? 0;
320
+ const high = result.ci.high ?? 0;
321
+ const includesZero = low <= 0 && high >= 0;
322
+ const zeroClause = includesZero
323
+ ? "includes zero (not distinguishable)"
324
+ : "excludes zero (distinguishable)";
325
+ const subject =
326
+ armB !== null ? `the difference in ${statLabel} (${comparisonLabel})` : `${statLabel}`;
327
+ summary =
328
+ `Paired comparison: ${subject} is ${formatCurrency(point)}, ` +
329
+ `${(ciLevel * 100).toFixed(0)}% CI [${formatCurrency(low)}, ${formatCurrency(high)}] -- ${zeroClause}.`;
330
+ }
331
+
332
+ const data: Record<string, unknown> = {
333
+ comparison: {
334
+ strategyA,
335
+ strategyB: strategyB ?? null,
336
+ mode: armB !== null ? "two-arm" : "single-arm-vs-zero",
337
+ description: comparisonLabel,
338
+ },
339
+ statistic,
340
+ point: result.point,
341
+ ci: result.ci,
342
+ status: result.status,
343
+ refusal: refusalMessage,
344
+ effectiveN: result.effectiveN,
345
+ blockDays: result.blockDays,
346
+ blockDaysDerivation: blockDaysNote,
347
+ overlapWindow: result.overlapWindow,
348
+ observedDays: {
349
+ armA: countObserved(armA),
350
+ armB: armB !== null ? countObserved(armB) : null,
351
+ },
352
+ tradeCounts: {
353
+ armA: armATrades.length,
354
+ armB: armBTrades !== null ? armBTrades.length : null,
355
+ },
356
+ sensitivity: result.sensitivity,
357
+ resamples,
358
+ seed: result.seed,
359
+ ciLevel,
360
+ attributionMethodology: ATTRIBUTION_NOTE,
361
+ };
362
+
363
+ return { summary, data, result };
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // Tool registration
368
+ // ---------------------------------------------------------------------------
369
+
370
+ /**
371
+ * Register the paired_bootstrap_comparison MCP tool.
372
+ */
373
+ export function registerPairedComparisonTool(server: McpServer, baseDir: string): void {
374
+ server.registerTool(
375
+ "paired_bootstrap_comparison",
376
+ {
377
+ description:
378
+ "Honest confidence intervals for 'is strategy A actually different from strategy B " +
379
+ "(or from zero)' -- day-block resampling that accounts for multi-day positions, dormant " +
380
+ "periods, and paired comparison on shared days. " +
381
+ ATTRIBUTION_NOTE,
382
+ inputSchema: pairedComparisonInputSchema,
383
+ },
384
+ withSyncedBlock(baseDir, async (input: PairedComparisonInput) => {
385
+ try {
386
+ const { blockId, strategyA, strategyB, statistic, dateRange } = input;
387
+ const block = await loadBlock(baseDir, blockId);
388
+
389
+ const inRange = (trades: Trade[]): Trade[] =>
390
+ dateRange ? filterByDateRange(trades, dateRange.start, dateRange.end) : trades;
391
+
392
+ const blockTrades = inRange(block.trades);
393
+ const armATrades = inRange(filterByStrategy(block.trades, strategyA));
394
+ const armBTrades =
395
+ strategyB !== undefined ? inRange(filterByStrategy(block.trades, strategyB)) : null;
396
+
397
+ const { summary, data } = runPairedBootstrapComparison({
398
+ armATrades,
399
+ armBTrades,
400
+ blockTrades,
401
+ statistic,
402
+ ciLevel: input.ciLevel,
403
+ resamples: input.resamples,
404
+ seed: input.seed,
405
+ blockDays: input.blockDays,
406
+ effectiveNFloorBlocks: input.effectiveNFloorBlocks,
407
+ strategyA,
408
+ strategyB,
409
+ });
410
+
411
+ return createToolOutput(summary, { blockId, ...data });
412
+ } catch (error) {
413
+ return {
414
+ content: [
415
+ {
416
+ type: "text" as const,
417
+ text: `Error running paired bootstrap comparison: ${(error as Error).message}`,
418
+ },
419
+ ],
420
+ isError: true as const,
421
+ };
422
+ }
423
+ }),
424
+ );
425
+ }