tradeblocks-mcp 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +82 -68
  2. package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
  3. package/dist/chunk-27S67XW3.js.map +1 -0
  4. package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
  5. package/dist/chunk-2E63THNI.js.map +1 -0
  6. package/dist/{chunk-BKQ4PM4Y.js → chunk-6S37CXUA.js} +11 -13
  7. package/dist/chunk-6S37CXUA.js.map +1 -0
  8. package/dist/{chunk-PRAYH3RT.js → chunk-AP7IHUUR.js} +29 -12
  9. package/dist/chunk-AP7IHUUR.js.map +1 -0
  10. package/dist/{chunk-QTTR7AAW.js → chunk-C6LL746C.js} +67 -84
  11. package/dist/{chunk-QTTR7AAW.js.map → chunk-C6LL746C.js.map} +1 -1
  12. package/dist/{chunk-W2PP3LEH.js → chunk-SEUZYQGQ.js} +17 -8
  13. package/dist/chunk-SEUZYQGQ.js.map +1 -0
  14. package/dist/{chunk-4BLCXNQ6.js → chunk-UBUC5A66.js} +105 -114
  15. package/dist/chunk-UBUC5A66.js.map +1 -0
  16. package/dist/{chunk-XXYOUIZY.js → chunk-VDU25Z6X.js} +63 -80
  17. package/dist/chunk-VDU25Z6X.js.map +1 -0
  18. package/dist/daily-log-processor-BY3ISY6K.js +9 -0
  19. package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
  20. package/dist/iv-solver-worker.js +9 -1
  21. package/dist/market-provider-H3ARIVZ4.js +16 -0
  22. package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
  23. package/dist/{sync-PO4IPCYV.js → sync-EVLKB3ZJ.js} +7 -7
  24. package/dist/test-exports.js +358 -254
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-EYA3I3XB.js +9 -0
  27. package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
  28. package/manifest.json +3 -11
  29. package/package.json +1 -1
  30. package/server/{chunk-NZO6PT64.js → chunk-4P7D7YZP.js} +102 -113
  31. package/server/chunk-4P7D7YZP.js.map +1 -0
  32. package/server/{chunk-72GKJE2U.js → chunk-BOPHW5M6.js} +521 -235
  33. package/server/chunk-BOPHW5M6.js.map +1 -0
  34. package/server/{chunk-WA5AAPCH.js → chunk-GH2552SE.js} +17 -8
  35. package/server/chunk-GH2552SE.js.map +1 -0
  36. package/server/{chunk-5EBXHT6C.js → chunk-OBYKFW2B.js} +11 -13
  37. package/server/chunk-OBYKFW2B.js.map +1 -0
  38. package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
  39. package/server/chunk-T66KH2XH.js.map +1 -0
  40. package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
  41. package/server/chunk-W5E7FHC4.js.map +1 -0
  42. package/server/{chunk-CSDVJPBB.js → chunk-YUCOAJ4Z.js} +29 -12
  43. package/server/chunk-YUCOAJ4Z.js.map +1 -0
  44. package/server/{chunk-PNKG7RY7.js → chunk-ZBJCF4ZG.js} +63 -80
  45. package/server/chunk-ZBJCF4ZG.js.map +1 -0
  46. package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
  47. package/server/config-DK7KOMNL.js.map +1 -0
  48. package/server/daily-log-processor-ENEUT22A.js +10 -0
  49. package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
  50. package/server/http-server.js +2 -5
  51. package/server/index.js +1771 -1538
  52. package/server/index.js.map +1 -1
  53. package/server/iv-solver-worker.js +9 -1
  54. package/server/market-provider-B437HKLW.js +17 -0
  55. package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
  56. package/server/{sync-QFI5L7S7.js → sync-V25UQJA3.js} +7 -7
  57. package/server/trade-processor-L3PIQ5TG.js +10 -0
  58. package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
  59. package/src/auth/clients-store.ts +4 -4
  60. package/src/auth/code-store.ts +1 -1
  61. package/src/auth/config.ts +11 -12
  62. package/src/auth/login-page.ts +8 -10
  63. package/src/auth/provider.ts +35 -30
  64. package/src/auth/token.ts +17 -15
  65. package/src/db/backtest-schemas.ts +12 -4
  66. package/src/db/connection.ts +107 -40
  67. package/src/db/index.ts +37 -5
  68. package/src/db/json-adapters.ts +22 -31
  69. package/src/db/json-migration.ts +11 -15
  70. package/src/db/json-store.ts +5 -3
  71. package/src/db/market-datasets.ts +48 -15
  72. package/src/db/market-schemas.ts +11 -5
  73. package/src/db/market-views.ts +119 -32
  74. package/src/db/parquet-writer.ts +7 -7
  75. package/src/db/profile-schemas.ts +39 -22
  76. package/src/db/schemas.ts +2 -2
  77. package/src/http-server.ts +7 -12
  78. package/src/index.ts +8 -9
  79. package/src/market/ingestor/index.ts +1 -4
  80. package/src/market/ingestor/market-ingestor.ts +140 -67
  81. package/src/market/ingestor/types.ts +1 -3
  82. package/src/market/stores/chain-sql.ts +2 -8
  83. package/src/market/stores/chain-store.ts +1 -4
  84. package/src/market/stores/duckdb-chain-store.ts +3 -14
  85. package/src/market/stores/duckdb-enriched-store.ts +3 -13
  86. package/src/market/stores/duckdb-quote-store.ts +10 -27
  87. package/src/market/stores/duckdb-spot-store.ts +4 -20
  88. package/src/market/stores/enriched-sql.ts +3 -9
  89. package/src/market/stores/enriched-store.ts +2 -2
  90. package/src/market/stores/parquet-chain-store.ts +5 -22
  91. package/src/market/stores/parquet-enriched-store.ts +4 -16
  92. package/src/market/stores/parquet-oi-daily-store.ts +3 -14
  93. package/src/market/stores/parquet-quote-store.ts +9 -29
  94. package/src/market/stores/parquet-spot-store.ts +14 -33
  95. package/src/market/stores/quote-store.ts +2 -10
  96. package/src/market/stores/spot-sql.ts +3 -15
  97. package/src/market/stores/spot-store.ts +1 -1
  98. package/src/market/tickers/defaults.json +6 -6
  99. package/src/market/tickers/loader.ts +2 -7
  100. package/src/market/tickers/registry.ts +5 -15
  101. package/src/market/tickers/schemas.ts +2 -6
  102. package/src/models/strategy-profile.ts +39 -39
  103. package/src/sync/block-sync.ts +44 -65
  104. package/src/sync/index.ts +1 -4
  105. package/src/sync/metadata.ts +14 -20
  106. package/src/test-exports.ts +127 -123
  107. package/src/tools/analysis.ts +180 -159
  108. package/src/tools/batch-exit-analysis.ts +100 -59
  109. package/src/tools/blocks/analysis.ts +116 -101
  110. package/src/tools/blocks/comparison.ts +281 -364
  111. package/src/tools/blocks/core.ts +208 -253
  112. package/src/tools/blocks/health.ts +571 -563
  113. package/src/tools/blocks/similarity.ts +316 -283
  114. package/src/tools/edge-decay.ts +124 -197
  115. package/src/tools/exit-analysis.ts +130 -77
  116. package/src/tools/greeks-attribution.ts +84 -35
  117. package/src/tools/guides.ts +4 -6
  118. package/src/tools/imports.ts +9 -12
  119. package/src/tools/market-data.ts +1067 -757
  120. package/src/tools/market-enrichment.ts +3 -3
  121. package/src/tools/market-fetch.ts +148 -67
  122. package/src/tools/market-imports.ts +12 -12
  123. package/src/tools/middleware/sync-middleware.ts +5 -6
  124. package/src/tools/performance.ts +185 -302
  125. package/src/tools/profile-analysis.ts +52 -66
  126. package/src/tools/profiles.ts +106 -69
  127. package/src/tools/regime-advisor.ts +20 -45
  128. package/src/tools/replay.ts +81 -77
  129. package/src/tools/reports/discrepancies.ts +298 -328
  130. package/src/tools/reports/fields.ts +7 -25
  131. package/src/tools/reports/helpers.ts +18 -49
  132. package/src/tools/reports/predictive.ts +27 -70
  133. package/src/tools/reports/slippage-trends.ts +315 -345
  134. package/src/tools/reports/slippage.ts +1 -4
  135. package/src/tools/reports/strategy-matches.ts +399 -441
  136. package/src/tools/schema.ts +43 -40
  137. package/src/tools/shared/filters.ts +3 -9
  138. package/src/tools/snapshot.ts +9 -30
  139. package/src/tools/sql.ts +15 -14
  140. package/src/tools/tickers.ts +1 -4
  141. package/src/utils/batch-exit-analysis.ts +31 -42
  142. package/src/utils/black-scholes.ts +39 -29
  143. package/src/utils/block-loader.ts +68 -82
  144. package/src/utils/calibration-probe.ts +1 -2
  145. package/src/utils/chain-loader.ts +3 -3
  146. package/src/utils/csv-discovery.ts +16 -22
  147. package/src/utils/data-quality.ts +24 -36
  148. package/src/utils/exit-triggers.ts +91 -96
  149. package/src/utils/field-timing.ts +94 -79
  150. package/src/utils/filter-predicates.ts +13 -9
  151. package/src/utils/flatfile-importer.ts +94 -64
  152. package/src/utils/greeks-decomposition.ts +152 -100
  153. package/src/utils/iv-solver-pool.ts +55 -25
  154. package/src/utils/iv-solver-worker.ts +5 -5
  155. package/src/utils/market-enricher.ts +528 -497
  156. package/src/utils/market-importer.ts +31 -12
  157. package/src/utils/market-provider.ts +21 -23
  158. package/src/utils/massive-tier.ts +5 -7
  159. package/src/utils/migrate-option-data-helpers.ts +2 -8
  160. package/src/utils/option-quote-greeks.ts +25 -31
  161. package/src/utils/option-time.ts +4 -8
  162. package/src/utils/output-formatter.ts +1 -4
  163. package/src/utils/provider-capabilities.ts +1 -4
  164. package/src/utils/providers/massive.ts +59 -93
  165. package/src/utils/providers/thetadata/backfill.ts +14 -23
  166. package/src/utils/providers/thetadata/client.ts +12 -8
  167. package/src/utils/providers/thetadata/decode.ts +2 -20
  168. package/src/utils/providers/thetadata/endpoints.ts +32 -40
  169. package/src/utils/providers/thetadata/join.ts +11 -10
  170. package/src/utils/providers/thetadata/proto.ts +12 -10
  171. package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
  172. package/src/utils/providers/thetadata.ts +11 -10
  173. package/src/utils/quote-enricher.ts +4 -4
  174. package/src/utils/quote-parquet-projection.ts +3 -11
  175. package/src/utils/sample-date-selector.ts +3 -5
  176. package/src/utils/schema-metadata.ts +102 -70
  177. package/src/utils/ticker.ts +5 -9
  178. package/src/utils/trade-replay.ts +77 -68
  179. package/dist/chunk-4BLCXNQ6.js.map +0 -1
  180. package/dist/chunk-BKQ4PM4Y.js.map +0 -1
  181. package/dist/chunk-FGZH632F.js.map +0 -1
  182. package/dist/chunk-LDKTV7GW.js.map +0 -1
  183. package/dist/chunk-PRAYH3RT.js.map +0 -1
  184. package/dist/chunk-W2PP3LEH.js.map +0 -1
  185. package/dist/chunk-XXYOUIZY.js.map +0 -1
  186. package/dist/daily-log-processor-6MWJ23JK.js +0 -9
  187. package/dist/market-provider-VDRJUEF2.js +0 -16
  188. package/dist/trade-processor-NHU4VWRX.js +0 -9
  189. package/server/chunk-5EBXHT6C.js.map +0 -1
  190. package/server/chunk-72GKJE2U.js.map +0 -1
  191. package/server/chunk-CSDVJPBB.js.map +0 -1
  192. package/server/chunk-FBNDMCT5.js.map +0 -1
  193. package/server/chunk-NRFXAJF7.js.map +0 -1
  194. package/server/chunk-NZO6PT64.js.map +0 -1
  195. package/server/chunk-PNKG7RY7.js.map +0 -1
  196. package/server/chunk-WA5AAPCH.js.map +0 -1
  197. package/server/config-6IZXEFEX.js.map +0 -1
  198. package/server/daily-log-processor-Y3PVSVBM.js +0 -10
  199. package/server/market-provider-VOYYVYWT.js +0 -17
  200. package/server/trade-processor-JWVS37KM.js +0 -10
  201. /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-BY3ISY6K.js.map} +0 -0
  202. /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
  203. /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-H3ARIVZ4.js.map} +0 -0
  204. /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
  205. /package/dist/{sync-PO4IPCYV.js.map → sync-EVLKB3ZJ.js.map} +0 -0
  206. /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-EYA3I3XB.js.map} +0 -0
  207. /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
  208. /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-ENEUT22A.js.map} +0 -0
  209. /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
  210. /package/server/{market-provider-VOYYVYWT.js.map → market-provider-B437HKLW.js.map} +0 -0
  211. /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
  212. /package/server/{sync-QFI5L7S7.js.map → sync-V25UQJA3.js.map} +0 -0
  213. /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-L3PIQ5TG.js.map} +0 -0
  214. /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
@@ -7,21 +7,14 @@
7
7
  import { z } from "zod";
8
8
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { loadBlock } from "../../utils/block-loader.ts";
10
- import {
11
- createToolOutput,
12
- formatPercent,
13
- formatRatio,
14
- } from "../../utils/output-formatter.ts";
10
+ import { createToolOutput, formatPercent, formatRatio } from "../../utils/output-formatter.ts";
15
11
  import {
16
12
  PortfolioStatsCalculator,
17
13
  calculateCorrelationMatrix,
18
14
  performTailRiskAnalysis,
19
15
  } from "@tradeblocks/lib";
20
16
  import type { Trade } from "@tradeblocks/lib";
21
- import {
22
- filterByDateRange,
23
- filterDailyLogsByDateRange,
24
- } from "../shared/filters.ts";
17
+ import { filterByDateRange, filterDailyLogsByDateRange } from "../shared/filters.ts";
25
18
  import { withSyncedBlock } from "../middleware/sync-middleware.ts";
26
19
  import { getConnection } from "../../db/connection.ts";
27
20
  import { getProfile } from "../../db/profile-schemas.ts";
@@ -38,10 +31,7 @@ const SIMILARITY_DEFAULTS = {
38
31
  /**
39
32
  * Register similarity block tools
40
33
  */
41
- export function registerSimilarityBlockTools(
42
- server: McpServer,
43
- baseDir: string
44
- ): void {
34
+ export function registerSimilarityBlockTools(server: McpServer, baseDir: string): void {
45
35
  const calculator = new PortfolioStatsCalculator();
46
36
 
47
37
  // Tool 10: strategy_similarity
@@ -58,7 +48,7 @@ export function registerSimilarityBlockTools(
58
48
  .max(1)
59
49
  .default(SIMILARITY_DEFAULTS.correlationThreshold)
60
50
  .describe(
61
- `Minimum correlation to flag as similar (default: ${SIMILARITY_DEFAULTS.correlationThreshold})`
51
+ `Minimum correlation to flag as similar (default: ${SIMILARITY_DEFAULTS.correlationThreshold})`,
62
52
  ),
63
53
  tailDependenceThreshold: z
64
54
  .number()
@@ -66,7 +56,7 @@ export function registerSimilarityBlockTools(
66
56
  .max(1)
67
57
  .default(SIMILARITY_DEFAULTS.tailDependenceThreshold)
68
58
  .describe(
69
- `Minimum tail dependence to flag as high joint risk (default: ${SIMILARITY_DEFAULTS.tailDependenceThreshold})`
59
+ `Minimum tail dependence to flag as high joint risk (default: ${SIMILARITY_DEFAULTS.tailDependenceThreshold})`,
70
60
  ),
71
61
  method: z
72
62
  .enum(["kendall", "spearman", "pearson"])
@@ -78,7 +68,7 @@ export function registerSimilarityBlockTools(
78
68
  .min(1)
79
69
  .default(SIMILARITY_DEFAULTS.minSharedDays)
80
70
  .describe(
81
- `Minimum shared trading days for valid comparison (default: ${SIMILARITY_DEFAULTS.minSharedDays})`
71
+ `Minimum shared trading days for valid comparison (default: ${SIMILARITY_DEFAULTS.minSharedDays})`,
82
72
  ),
83
73
  topN: z
84
74
  .number()
@@ -87,7 +77,7 @@ export function registerSimilarityBlockTools(
87
77
  .max(50)
88
78
  .default(SIMILARITY_DEFAULTS.topN)
89
79
  .describe(
90
- `Number of most similar pairs to return (default: ${SIMILARITY_DEFAULTS.topN})`
80
+ `Number of most similar pairs to return (default: ${SIMILARITY_DEFAULTS.topN})`,
91
81
  ),
92
82
  }),
93
83
  },
@@ -102,11 +92,9 @@ export function registerSimilarityBlockTools(
102
92
  topN,
103
93
  }) => {
104
94
  // Apply defaults for optional parameters (zod defaults may not apply through MCP CLI)
105
- const corrThreshold =
106
- correlationThreshold ?? SIMILARITY_DEFAULTS.correlationThreshold;
95
+ const corrThreshold = correlationThreshold ?? SIMILARITY_DEFAULTS.correlationThreshold;
107
96
  const tailThreshold =
108
- tailDependenceThreshold ??
109
- SIMILARITY_DEFAULTS.tailDependenceThreshold;
97
+ tailDependenceThreshold ?? SIMILARITY_DEFAULTS.tailDependenceThreshold;
110
98
  const corrMethod = method ?? SIMILARITY_DEFAULTS.method;
111
99
  const minDays = minSharedDays ?? SIMILARITY_DEFAULTS.minSharedDays;
112
100
  const limit = topN ?? SIMILARITY_DEFAULTS.topN;
@@ -126,12 +114,10 @@ export function registerSimilarityBlockTools(
126
114
  };
127
115
  }
128
116
 
129
- // Get unique strategies
130
- const strategies = Array.from(
131
- new Set(trades.map((t) => t.strategy))
132
- ).sort();
117
+ // Get unique strategies
118
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
133
119
 
134
- // Need at least 2 strategies for similarity analysis
120
+ // Need at least 2 strategies for similarity analysis
135
121
  if (strategies.length < 2) {
136
122
  return {
137
123
  content: [
@@ -144,187 +130,180 @@ export function registerSimilarityBlockTools(
144
130
  };
145
131
  }
146
132
 
147
- // Calculate correlation matrix using existing utility
148
- const correlationMatrix = calculateCorrelationMatrix(trades, {
149
- method: corrMethod,
150
- normalization: "raw",
151
- dateBasis: "opened",
152
- alignment: "shared",
153
- });
154
-
155
- // Calculate tail risk using existing utility
156
- const tailRisk = performTailRiskAnalysis(trades, {
157
- normalization: "raw",
158
- dateBasis: "opened",
159
- minTradingDays: minDays,
160
- });
161
-
162
- // Calculate overlap scores: count shared trading days / total unique days
163
- // Group trades by strategy and date
164
- const strategyDates: Record<string, Set<string>> = {};
165
- for (const trade of trades) {
166
- if (!trade.strategy || !trade.dateOpened) continue;
167
- if (!strategyDates[trade.strategy]) {
168
- strategyDates[trade.strategy] = new Set();
133
+ // Calculate correlation matrix using existing utility
134
+ const correlationMatrix = calculateCorrelationMatrix(trades, {
135
+ method: corrMethod,
136
+ normalization: "raw",
137
+ dateBasis: "opened",
138
+ alignment: "shared",
139
+ });
140
+
141
+ // Calculate tail risk using existing utility
142
+ const tailRisk = performTailRiskAnalysis(trades, {
143
+ normalization: "raw",
144
+ dateBasis: "opened",
145
+ minTradingDays: minDays,
146
+ });
147
+
148
+ // Calculate overlap scores: count shared trading days / total unique days
149
+ // Group trades by strategy and date
150
+ const strategyDates: Record<string, Set<string>> = {};
151
+ for (const trade of trades) {
152
+ if (!trade.strategy || !trade.dateOpened) continue;
153
+ if (!strategyDates[trade.strategy]) {
154
+ strategyDates[trade.strategy] = new Set();
155
+ }
156
+ // Extract date key from dateOpened
157
+ const dateKey = trade.dateOpened.toISOString().split("T")[0];
158
+ strategyDates[trade.strategy].add(dateKey);
169
159
  }
170
- // Extract date key from dateOpened
171
- const dateKey = trade.dateOpened.toISOString().split("T")[0];
172
- strategyDates[trade.strategy].add(dateKey);
173
- }
174
160
 
175
- // Build similarity pairs
176
- interface SimilarPair {
177
- strategyA: string;
178
- strategyB: string;
179
- correlation: number | null;
180
- tailDependence: number | null;
181
- overlapScore: number;
182
- compositeSimilarity: number | null;
183
- sharedTradingDays: number;
184
- flags: {
185
- isHighCorrelation: boolean;
186
- isHighTailDependence: boolean;
187
- isRedundant: boolean;
188
- };
189
- }
161
+ // Build similarity pairs
162
+ interface SimilarPair {
163
+ strategyA: string;
164
+ strategyB: string;
165
+ correlation: number | null;
166
+ tailDependence: number | null;
167
+ overlapScore: number;
168
+ compositeSimilarity: number | null;
169
+ sharedTradingDays: number;
170
+ flags: {
171
+ isHighCorrelation: boolean;
172
+ isHighTailDependence: boolean;
173
+ isRedundant: boolean;
174
+ };
175
+ }
190
176
 
191
- const pairs: SimilarPair[] = [];
192
- let redundantPairs = 0;
193
- let highCorrelationPairs = 0;
194
- let highTailDependencePairs = 0;
195
-
196
- // Iterate over unique strategy pairs (i < j)
197
- for (let i = 0; i < strategies.length; i++) {
198
- for (let j = i + 1; j < strategies.length; j++) {
199
- const strategyA = strategies[i];
200
- const strategyB = strategies[j];
201
-
202
- // Get correlation from matrix
203
- const idxA = correlationMatrix.strategies.indexOf(strategyA);
204
- const idxB = correlationMatrix.strategies.indexOf(strategyB);
205
- const correlation =
206
- idxA >= 0 && idxB >= 0 && correlationMatrix.correlationData[idxA]
207
- ? correlationMatrix.correlationData[idxA][idxB]
208
- : null;
209
- const sharedDaysFromCorr =
210
- idxA >= 0 && idxB >= 0 && correlationMatrix.sampleSizes[idxA]
211
- ? correlationMatrix.sampleSizes[idxA][idxB]
212
- : 0;
213
-
214
- // Get tail dependence from jointTailRiskMatrix
215
- const tailIdxA = tailRisk.strategies.indexOf(strategyA);
216
- const tailIdxB = tailRisk.strategies.indexOf(strategyB);
217
- let tailDependence: number | null = null;
218
- if (
219
- tailIdxA >= 0 &&
220
- tailIdxB >= 0 &&
221
- tailRisk.jointTailRiskMatrix[tailIdxA] &&
222
- tailRisk.jointTailRiskMatrix[tailIdxB]
223
- ) {
224
- // Average both directions since matrix can be asymmetric
225
- const valAB = tailRisk.jointTailRiskMatrix[tailIdxA][tailIdxB];
226
- const valBA = tailRisk.jointTailRiskMatrix[tailIdxB][tailIdxA];
227
- if (!Number.isNaN(valAB) && !Number.isNaN(valBA)) {
228
- tailDependence = (valAB + valBA) / 2;
177
+ const pairs: SimilarPair[] = [];
178
+ let redundantPairs = 0;
179
+ let highCorrelationPairs = 0;
180
+ let highTailDependencePairs = 0;
181
+
182
+ // Iterate over unique strategy pairs (i < j)
183
+ for (let i = 0; i < strategies.length; i++) {
184
+ for (let j = i + 1; j < strategies.length; j++) {
185
+ const strategyA = strategies[i];
186
+ const strategyB = strategies[j];
187
+
188
+ // Get correlation from matrix
189
+ const idxA = correlationMatrix.strategies.indexOf(strategyA);
190
+ const idxB = correlationMatrix.strategies.indexOf(strategyB);
191
+ const correlation =
192
+ idxA >= 0 && idxB >= 0 && correlationMatrix.correlationData[idxA]
193
+ ? correlationMatrix.correlationData[idxA][idxB]
194
+ : null;
195
+ const sharedDaysFromCorr =
196
+ idxA >= 0 && idxB >= 0 && correlationMatrix.sampleSizes[idxA]
197
+ ? correlationMatrix.sampleSizes[idxA][idxB]
198
+ : 0;
199
+
200
+ // Get tail dependence from jointTailRiskMatrix
201
+ const tailIdxA = tailRisk.strategies.indexOf(strategyA);
202
+ const tailIdxB = tailRisk.strategies.indexOf(strategyB);
203
+ let tailDependence: number | null = null;
204
+ if (
205
+ tailIdxA >= 0 &&
206
+ tailIdxB >= 0 &&
207
+ tailRisk.jointTailRiskMatrix[tailIdxA] &&
208
+ tailRisk.jointTailRiskMatrix[tailIdxB]
209
+ ) {
210
+ // Average both directions since matrix can be asymmetric
211
+ const valAB = tailRisk.jointTailRiskMatrix[tailIdxA][tailIdxB];
212
+ const valBA = tailRisk.jointTailRiskMatrix[tailIdxB][tailIdxA];
213
+ if (!Number.isNaN(valAB) && !Number.isNaN(valBA)) {
214
+ tailDependence = (valAB + valBA) / 2;
215
+ }
229
216
  }
230
- }
231
217
 
232
- // Calculate overlap score
233
- const datesA = strategyDates[strategyA] || new Set();
234
- const datesB = strategyDates[strategyB] || new Set();
235
- const allDates = new Set([...datesA, ...datesB]);
236
- const sharedDates = [...datesA].filter((d) => datesB.has(d)).length;
237
- const overlapScore =
238
- allDates.size > 0 ? sharedDates / allDates.size : 0;
239
-
240
- // Use sharedDaysFromCorr or calculate from overlap
241
- const sharedTradingDays =
242
- sharedDaysFromCorr > 0 ? sharedDaysFromCorr : sharedDates;
243
-
244
- // Calculate composite similarity score (weighted average)
245
- // 50% correlation (absolute value), 30% tail dependence, 20% overlap score
246
- let compositeSimilarity: number | null = null;
247
- if (correlation !== null && !Number.isNaN(correlation)) {
248
- const corrComponent = Math.abs(correlation) * 0.5;
249
- const tailComponent =
250
- (tailDependence !== null ? tailDependence : 0) * 0.3;
251
- const overlapComponent = overlapScore * 0.2;
252
- compositeSimilarity =
253
- corrComponent + tailComponent + overlapComponent;
254
- }
218
+ // Calculate overlap score
219
+ const datesA = strategyDates[strategyA] || new Set();
220
+ const datesB = strategyDates[strategyB] || new Set();
221
+ const allDates = new Set([...datesA, ...datesB]);
222
+ const sharedDates = [...datesA].filter((d) => datesB.has(d)).length;
223
+ const overlapScore = allDates.size > 0 ? sharedDates / allDates.size : 0;
224
+
225
+ // Use sharedDaysFromCorr or calculate from overlap
226
+ const sharedTradingDays = sharedDaysFromCorr > 0 ? sharedDaysFromCorr : sharedDates;
227
+
228
+ // Calculate composite similarity score (weighted average)
229
+ // 50% correlation (absolute value), 30% tail dependence, 20% overlap score
230
+ let compositeSimilarity: number | null = null;
231
+ if (correlation !== null && !Number.isNaN(correlation)) {
232
+ const corrComponent = Math.abs(correlation) * 0.5;
233
+ const tailComponent = (tailDependence !== null ? tailDependence : 0) * 0.3;
234
+ const overlapComponent = overlapScore * 0.2;
235
+ compositeSimilarity = corrComponent + tailComponent + overlapComponent;
236
+ }
255
237
 
256
- // Determine flags
257
- const isHighCorrelation =
258
- correlation !== null &&
259
- !Number.isNaN(correlation) &&
260
- Math.abs(correlation) >= corrThreshold;
261
- const isHighTailDependence =
262
- tailDependence !== null && tailDependence >= tailThreshold;
263
- const isRedundant = isHighCorrelation && isHighTailDependence;
264
-
265
- // Only include pairs that meet minDays requirement
266
- if (sharedTradingDays >= minDays) {
267
- // Update counters (only for included pairs)
268
- if (isHighCorrelation) highCorrelationPairs++;
269
- if (isHighTailDependence) highTailDependencePairs++;
270
- if (isRedundant) redundantPairs++;
271
-
272
- pairs.push({
273
- strategyA,
274
- strategyB,
275
- correlation:
276
- correlation !== null && !Number.isNaN(correlation)
277
- ? correlation
278
- : null,
279
- tailDependence,
280
- overlapScore,
281
- compositeSimilarity,
282
- sharedTradingDays,
283
- flags: {
284
- isHighCorrelation,
285
- isHighTailDependence,
286
- isRedundant,
287
- },
288
- });
238
+ // Determine flags
239
+ const isHighCorrelation =
240
+ correlation !== null &&
241
+ !Number.isNaN(correlation) &&
242
+ Math.abs(correlation) >= corrThreshold;
243
+ const isHighTailDependence =
244
+ tailDependence !== null && tailDependence >= tailThreshold;
245
+ const isRedundant = isHighCorrelation && isHighTailDependence;
246
+
247
+ // Only include pairs that meet minDays requirement
248
+ if (sharedTradingDays >= minDays) {
249
+ // Update counters (only for included pairs)
250
+ if (isHighCorrelation) highCorrelationPairs++;
251
+ if (isHighTailDependence) highTailDependencePairs++;
252
+ if (isRedundant) redundantPairs++;
253
+
254
+ pairs.push({
255
+ strategyA,
256
+ strategyB,
257
+ correlation:
258
+ correlation !== null && !Number.isNaN(correlation) ? correlation : null,
259
+ tailDependence,
260
+ overlapScore,
261
+ compositeSimilarity,
262
+ sharedTradingDays,
263
+ flags: {
264
+ isHighCorrelation,
265
+ isHighTailDependence,
266
+ isRedundant,
267
+ },
268
+ });
269
+ }
289
270
  }
290
271
  }
291
- }
292
272
 
293
- // Sort by composite similarity (highest first), handling nulls
294
- pairs.sort((a, b) => {
295
- if (a.compositeSimilarity === null && b.compositeSimilarity === null)
296
- return 0;
297
- if (a.compositeSimilarity === null) return 1;
298
- if (b.compositeSimilarity === null) return -1;
299
- return b.compositeSimilarity - a.compositeSimilarity;
300
- });
301
-
302
- // Apply limit
303
- const topPairs = pairs.slice(0, limit);
304
-
305
- // Build summary line
306
- const mostSimilar = topPairs[0];
307
- const summary = `Strategy Similarity: ${blockId} | ${strategies.length} strategies | ${redundantPairs} redundant pairs | Most similar: ${mostSimilar ? `${mostSimilar.strategyA}-${mostSimilar.strategyB} (${mostSimilar.compositeSimilarity?.toFixed(2) ?? "N/A"})` : "N/A"}`;
308
-
309
- // Build structured data
310
- const structuredData = {
311
- blockId,
312
- options: {
313
- correlationThreshold: corrThreshold,
314
- tailDependenceThreshold: tailThreshold,
315
- method: corrMethod,
316
- minSharedDays: minDays,
317
- topN: limit,
318
- },
319
- strategySummary: {
320
- totalStrategies: strategies.length,
321
- totalPairs: (strategies.length * (strategies.length - 1)) / 2,
322
- redundantPairs,
323
- highCorrelationPairs,
324
- highTailDependencePairs,
325
- },
326
- similarPairs: topPairs,
327
- };
273
+ // Sort by composite similarity (highest first), handling nulls
274
+ pairs.sort((a, b) => {
275
+ if (a.compositeSimilarity === null && b.compositeSimilarity === null) return 0;
276
+ if (a.compositeSimilarity === null) return 1;
277
+ if (b.compositeSimilarity === null) return -1;
278
+ return b.compositeSimilarity - a.compositeSimilarity;
279
+ });
280
+
281
+ // Apply limit
282
+ const topPairs = pairs.slice(0, limit);
283
+
284
+ // Build summary line
285
+ const mostSimilar = topPairs[0];
286
+ const summary = `Strategy Similarity: ${blockId} | ${strategies.length} strategies | ${redundantPairs} redundant pairs | Most similar: ${mostSimilar ? `${mostSimilar.strategyA}-${mostSimilar.strategyB} (${mostSimilar.compositeSimilarity?.toFixed(2) ?? "N/A"})` : "N/A"}`;
287
+
288
+ // Build structured data
289
+ const structuredData = {
290
+ blockId,
291
+ options: {
292
+ correlationThreshold: corrThreshold,
293
+ tailDependenceThreshold: tailThreshold,
294
+ method: corrMethod,
295
+ minSharedDays: minDays,
296
+ topN: limit,
297
+ },
298
+ strategySummary: {
299
+ totalStrategies: strategies.length,
300
+ totalPairs: (strategies.length * (strategies.length - 1)) / 2,
301
+ redundantPairs,
302
+ highCorrelationPairs,
303
+ highTailDependencePairs,
304
+ },
305
+ similarPairs: topPairs,
306
+ };
328
307
 
329
308
  return createToolOutput(summary, structuredData);
330
309
  } catch (error) {
@@ -338,8 +317,8 @@ export function registerSimilarityBlockTools(
338
317
  isError: true as const,
339
318
  };
340
319
  }
341
- }
342
- )
320
+ },
321
+ ),
343
322
  );
344
323
 
345
324
  // Tool 11: what_if_scaling
@@ -349,29 +328,58 @@ export function registerSimilarityBlockTools(
349
328
  description:
350
329
  "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.",
351
330
  inputSchema: z.object({
352
- blockId: z.string().describe("Block folder name (required for single-strategy mode, optional default for multi-strategy mode)"),
331
+ blockId: z
332
+ .string()
333
+ .describe(
334
+ "Block folder name (required for single-strategy mode, optional default for multi-strategy mode)",
335
+ ),
353
336
  strategyWeights: z
354
337
  .record(z.string(), z.number().min(0).max(2))
355
338
  .optional()
356
339
  .describe(
357
- 'Weight per strategy, e.g., {"5/7 17Δ": 0.5}. Unspecified strategies default to 1.0. Weight 0 = exclude strategy entirely. Max weight: 2.0. Ignored when strategies array is provided.'
340
+ 'Weight per strategy, e.g., {"5/7 17Δ": 0.5}. Unspecified strategies default to 1.0. Weight 0 = exclude strategy entirely. Max weight: 2.0. Ignored when strategies array is provided.',
358
341
  ),
359
- strategies: z.array(z.object({
360
- strategyName: z.string().describe("Strategy name matching a stored profile"),
361
- blockId: z.string().describe("Block ID to source trades from (overrides top-level blockId for this strategy)"),
362
- scaleFactor: z.number().min(0).max(5).describe("Scale factor for this strategy (1.0 = current allocation)"),
363
- })).optional().describe("Multi-strategy mode: array of strategies with per-strategy block source and scale. When provided, ignores strategyWeights."),
364
- showUncapped: z.boolean().optional().default(false).describe("When true, also run without maxContractsPerTrade ceiling for side-by-side comparison"),
365
- startDate: z
366
- .string()
342
+ strategies: z
343
+ .array(
344
+ z.object({
345
+ strategyName: z.string().describe("Strategy name matching a stored profile"),
346
+ blockId: z
347
+ .string()
348
+ .describe(
349
+ "Block ID to source trades from (overrides top-level blockId for this strategy)",
350
+ ),
351
+ scaleFactor: z
352
+ .number()
353
+ .min(0)
354
+ .max(5)
355
+ .describe("Scale factor for this strategy (1.0 = current allocation)"),
356
+ }),
357
+ )
367
358
  .optional()
368
- .describe("Start date filter (YYYY-MM-DD)"),
359
+ .describe(
360
+ "Multi-strategy mode: array of strategies with per-strategy block source and scale. When provided, ignores strategyWeights.",
361
+ ),
362
+ showUncapped: z
363
+ .boolean()
364
+ .optional()
365
+ .default(false)
366
+ .describe(
367
+ "When true, also run without maxContractsPerTrade ceiling for side-by-side comparison",
368
+ ),
369
+ startDate: z.string().optional().describe("Start date filter (YYYY-MM-DD)"),
369
370
  endDate: z.string().optional().describe("End date filter (YYYY-MM-DD)"),
370
371
  }),
371
372
  },
372
373
  withSyncedBlock(
373
374
  baseDir,
374
- async ({ blockId, strategyWeights, strategies: strategiesInput, showUncapped, startDate, endDate }) => {
375
+ async ({
376
+ blockId,
377
+ strategyWeights,
378
+ strategies: strategiesInput,
379
+ showUncapped,
380
+ startDate,
381
+ endDate,
382
+ }) => {
375
383
  try {
376
384
  const useMultiStrategy = strategiesInput && strategiesInput.length > 0;
377
385
  const doShowUncapped = showUncapped ?? false;
@@ -379,7 +387,7 @@ export function registerSimilarityBlockTools(
379
387
  // Helper: try to get a profile for a strategy (best-effort)
380
388
  async function tryGetProfile(
381
389
  strategyBlockId: string,
382
- strategyName: string
390
+ strategyName: string,
383
391
  ): Promise<{ profile: StrategyProfile | null; status: "found" | "not_found" | "error" }> {
384
392
  try {
385
393
  const conn = await getConnection(baseDir);
@@ -395,7 +403,7 @@ export function registerSimilarityBlockTools(
395
403
  // Helper: filter trades by strategy with single-strategy fallback
396
404
  function filterTradesByStrategy(allTrades: Trade[], strategyName: string): Trade[] {
397
405
  const matched = allTrades.filter(
398
- (t) => t.strategy.toLowerCase() === strategyName.toLowerCase()
406
+ (t) => t.strategy.toLowerCase() === strategyName.toLowerCase(),
399
407
  );
400
408
  if (matched.length > 0) return matched;
401
409
  // Single-strategy block fallback
@@ -416,7 +424,7 @@ export function registerSimilarityBlockTools(
416
424
  function buildScaledTrades(
417
425
  tradesToScale: Trade[],
418
426
  weights: Record<string, number>,
419
- maxContractsCeilings?: Record<string, number>
427
+ maxContractsCeilings?: Record<string, number>,
420
428
  ): { scaledTrades: ScaledTrade[]; cappedStrategies: Set<string> } {
421
429
  const scaled: ScaledTrade[] = [];
422
430
  const cappedStrategies = new Set<string>();
@@ -452,7 +460,7 @@ export function registerSimilarityBlockTools(
452
460
  // Helper: build modified Trade[] with recalculated fundsAtClose for portfolio stats
453
461
  function buildModifiedTrades(
454
462
  scaledTrades: ScaledTrade[],
455
- originalTrades: Trade[]
463
+ originalTrades: Trade[],
456
464
  ): Trade[] {
457
465
  const sortedOriginal = [...originalTrades]
458
466
  .filter((t) => t.dateClosed && t.fundsAtClose !== undefined)
@@ -498,7 +506,7 @@ export function registerSimilarityBlockTools(
498
506
  // Helper: calculate comparison deltas
499
507
  function calcDelta(
500
508
  original: number | null,
501
- scaled: number | null
509
+ scaled: number | null,
502
510
  ): {
503
511
  original: number | null;
504
512
  scaled: number | null;
@@ -509,8 +517,7 @@ export function registerSimilarityBlockTools(
509
517
  return { original, scaled, delta: null, deltaPct: null };
510
518
  }
511
519
  const delta = scaled - original;
512
- const deltaPct =
513
- original !== 0 ? (delta / Math.abs(original)) * 100 : null;
520
+ const deltaPct = original !== 0 ? (delta / Math.abs(original)) * 100 : null;
514
521
  return { original, scaled, delta, deltaPct };
515
522
  }
516
523
 
@@ -538,11 +545,19 @@ export function registerSimilarityBlockTools(
538
545
  const allScaledTrades: ScaledTrade[] = [];
539
546
  const allOriginalTrades: Trade[] = [];
540
547
  const perStrategyBreakdown: MultiStrategyBreakdown[] = [];
541
- const dataAvailability: { strategy: string; blockId: string; profileStatus: string; profileBlockId?: string }[] = [];
548
+ const dataAvailability: {
549
+ strategy: string;
550
+ blockId: string;
551
+ profileStatus: string;
552
+ profileBlockId?: string;
553
+ }[] = [];
542
554
 
543
555
  for (const entry of strategiesInput!) {
544
556
  const sourceBlockId = entry.blockId || blockId;
545
- const { profile, status: profileStatus } = await tryGetProfile(sourceBlockId, entry.strategyName);
557
+ const { profile, status: profileStatus } = await tryGetProfile(
558
+ sourceBlockId,
559
+ entry.strategyName,
560
+ );
546
561
 
547
562
  // Determine which block to load trades from
548
563
  let tradeSourceBlockId = sourceBlockId;
@@ -600,7 +615,7 @@ export function registerSimilarityBlockTools(
600
615
  // Build weights and ceilings for this strategy's trades
601
616
  const weightsMap: Record<string, number> = {};
602
617
  const ceilings: Record<string, number> = {};
603
- const uniqueNames = Array.from(new Set(entryTrades.map(t => t.strategy)));
618
+ const uniqueNames = Array.from(new Set(entryTrades.map((t) => t.strategy)));
604
619
  for (const name of uniqueNames) {
605
620
  weightsMap[name] = entry.scaleFactor;
606
621
  if (profile?.positionSizing?.maxContractsPerTrade !== undefined) {
@@ -609,7 +624,9 @@ export function registerSimilarityBlockTools(
609
624
  }
610
625
 
611
626
  const { scaledTrades: entryScaled, cappedStrategies } = buildScaledTrades(
612
- entryTrades, weightsMap, Object.keys(ceilings).length > 0 ? ceilings : undefined
627
+ entryTrades,
628
+ weightsMap,
629
+ Object.keys(ceilings).length > 0 ? ceilings : undefined,
613
630
  );
614
631
 
615
632
  allOriginalTrades.push(...entryTrades);
@@ -644,7 +661,8 @@ export function registerSimilarityBlockTools(
644
661
 
645
662
  if (profile?.ignoreMarginReq) {
646
663
  breakdown.ignoreMarginReq = true;
647
- breakdown.marginNote = "Strategy ignores margin requirements. Scaled notional exposure shown but buying power impact not estimated.";
664
+ breakdown.marginNote =
665
+ "Strategy ignores margin requirements. Scaled notional exposure shown but buying power impact not estimated.";
648
666
  breakdown.scaledNotionalExposure = Math.abs(scaledNetPl);
649
667
  }
650
668
 
@@ -654,20 +672,21 @@ export function registerSimilarityBlockTools(
654
672
  // Calculate contribution percentages
655
673
  const totalCombinedPl = perStrategyBreakdown.reduce((sum, b) => sum + b.scaledNetPl, 0);
656
674
  for (const b of perStrategyBreakdown) {
657
- b.plContributionPct = totalCombinedPl !== 0
658
- ? (b.scaledNetPl / Math.abs(totalCombinedPl)) * 100
659
- : 0;
675
+ b.plContributionPct =
676
+ totalCombinedPl !== 0 ? (b.scaledNetPl / Math.abs(totalCombinedPl)) * 100 : 0;
660
677
  }
661
678
 
662
679
  // Calculate combined portfolio stats
663
680
  const modifiedTrades = buildModifiedTrades(allScaledTrades, allOriginalTrades);
664
681
  const combinedStats = calculator.calculatePortfolioStats(
665
- modifiedTrades, undefined, true
682
+ modifiedTrades,
683
+ undefined,
684
+ true,
666
685
  );
667
686
 
668
687
  // Build uncapped comparison if requested and any strategy was capped
669
688
  let uncappedComparison: Record<string, unknown> | undefined;
670
- const anyCapped = perStrategyBreakdown.some(b => b.capped);
689
+ const anyCapped = perStrategyBreakdown.some((b) => b.capped);
671
690
  if (doShowUncapped && anyCapped) {
672
691
  const uncappedScaled: ScaledTrade[] = [];
673
692
  for (const entry of strategiesInput!) {
@@ -682,16 +701,22 @@ export function registerSimilarityBlockTools(
682
701
  let entryTrades = filterTradesByStrategy(entryBlock.trades, entry.strategyName);
683
702
  entryTrades = filterByDateRange(entryTrades, startDate, endDate);
684
703
  const wm: Record<string, number> = {};
685
- for (const name of new Set(entryTrades.map(t => t.strategy))) {
704
+ for (const name of new Set(entryTrades.map((t) => t.strategy))) {
686
705
  wm[name] = entry.scaleFactor;
687
706
  }
688
707
  const { scaledTrades: ust } = buildScaledTrades(entryTrades, wm);
689
708
  uncappedScaled.push(...ust);
690
- } catch { /* skip */ }
709
+ } catch {
710
+ /* skip */
711
+ }
691
712
  }
692
713
  if (uncappedScaled.length > 0) {
693
714
  const uncappedModified = buildModifiedTrades(uncappedScaled, allOriginalTrades);
694
- const uncappedStats = calculator.calculatePortfolioStats(uncappedModified, undefined, true);
715
+ const uncappedStats = calculator.calculatePortfolioStats(
716
+ uncappedModified,
717
+ undefined,
718
+ true,
719
+ );
695
720
  uncappedComparison = {
696
721
  sharpeRatio: uncappedStats.sharpeRatio,
697
722
  sortinoRatio: uncappedStats.sortinoRatio,
@@ -748,9 +773,7 @@ export function registerSimilarityBlockTools(
748
773
  }
749
774
 
750
775
  // Get all unique strategies
751
- const allStrategies = Array.from(
752
- new Set(trades.map((t) => t.strategy))
753
- ).sort();
776
+ const allStrategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
754
777
 
755
778
  // Build applied weights (default 1.0 for unspecified)
756
779
  const appliedWeights: Record<string, number> = {};
@@ -763,7 +786,7 @@ export function registerSimilarityBlockTools(
763
786
  if (strategyWeights) {
764
787
  for (const [strategy, weight] of Object.entries(strategyWeights)) {
765
788
  const matchedStrategy = allStrategies.find(
766
- (s) => s.toLowerCase() === strategy.toLowerCase()
789
+ (s) => s.toLowerCase() === strategy.toLowerCase(),
767
790
  );
768
791
  if (matchedStrategy) {
769
792
  appliedWeights[matchedStrategy] = weight;
@@ -773,9 +796,7 @@ export function registerSimilarityBlockTools(
773
796
  }
774
797
  }
775
798
 
776
- const allZeroWeight = Object.values(appliedWeights).every(
777
- (w) => w === 0
778
- );
799
+ const allZeroWeight = Object.values(appliedWeights).every((w) => w === 0);
779
800
  if (allZeroWeight) {
780
801
  return {
781
802
  content: [
@@ -789,11 +810,18 @@ export function registerSimilarityBlockTools(
789
810
  }
790
811
 
791
812
  // Profile-aware enhancements: look up profiles for each strategy (best-effort)
792
- const profileLookups: Record<string, { profile: StrategyProfile | null; status: "found" | "not_found" | "error" }> = {};
813
+ const profileLookups: Record<
814
+ string,
815
+ { profile: StrategyProfile | null; status: "found" | "not_found" | "error" }
816
+ > = {};
793
817
  const maxContractsCeilings: Record<string, number> = {};
794
818
  const dataSourceMap: Record<string, string> = {};
795
819
  let tradesToUse = trades;
796
- const backtestSubstitutions: { strategy: string; originalBlockId: string; backtestBlockId: string }[] = [];
820
+ const backtestSubstitutions: {
821
+ strategy: string;
822
+ originalBlockId: string;
823
+ backtestBlockId: string;
824
+ }[] = [];
797
825
 
798
826
  for (const strategy of allStrategies) {
799
827
  const lookup = await tryGetProfile(blockId, strategy);
@@ -810,7 +838,7 @@ export function registerSimilarityBlockTools(
810
838
 
811
839
  if (backtestTrades.length > 0) {
812
840
  tradesToUse = tradesToUse.filter(
813
- (t) => t.strategy.toLowerCase() !== strategy.toLowerCase()
841
+ (t) => t.strategy.toLowerCase() !== strategy.toLowerCase(),
814
842
  );
815
843
  tradesToUse = [...tradesToUse, ...backtestTrades];
816
844
  dataSourceMap[strategy] = "standalone_backtest";
@@ -833,9 +861,10 @@ export function registerSimilarityBlockTools(
833
861
  }
834
862
 
835
863
  // Calculate baseline portfolio metrics (from original trades, before substitution)
836
- const dailyLogs = block.dailyLogs && block.dailyLogs.length > 0
837
- ? filterDailyLogsByDateRange(block.dailyLogs, startDate, endDate)
838
- : undefined;
864
+ const dailyLogs =
865
+ block.dailyLogs && block.dailyLogs.length > 0
866
+ ? filterDailyLogsByDateRange(block.dailyLogs, startDate, endDate)
867
+ : undefined;
839
868
  const baselineStats = calculator.calculatePortfolioStats(
840
869
  trades,
841
870
  dailyLogs && dailyLogs.length > 0 ? dailyLogs : undefined,
@@ -846,26 +875,37 @@ export function registerSimilarityBlockTools(
846
875
  const { scaledTrades, cappedStrategies } = buildScaledTrades(
847
876
  tradesToUse,
848
877
  appliedWeights,
849
- hasCeilings ? maxContractsCeilings : undefined
878
+ hasCeilings ? maxContractsCeilings : undefined,
850
879
  );
851
880
 
852
881
  const modifiedTrades = buildModifiedTrades(scaledTrades, trades);
853
- const scaledStats = calculator.calculatePortfolioStats(
854
- modifiedTrades, undefined, true
855
- );
882
+ const scaledStats = calculator.calculatePortfolioStats(modifiedTrades, undefined, true);
856
883
 
857
884
  // Uncapped comparison if requested and any strategy was capped
858
885
  let uncappedComparison: Record<string, unknown> | undefined;
859
886
  if (doShowUncapped && cappedStrategies.size > 0) {
860
887
  const { scaledTrades: uncappedScaled } = buildScaledTrades(tradesToUse, appliedWeights);
861
888
  const uncappedModified = buildModifiedTrades(uncappedScaled, trades);
862
- const uncappedStats = calculator.calculatePortfolioStats(uncappedModified, undefined, true);
889
+ const uncappedStats = calculator.calculatePortfolioStats(
890
+ uncappedModified,
891
+ undefined,
892
+ true,
893
+ );
863
894
  uncappedComparison = {
864
- sharpeRatio: calcDelta(baselineStats.sharpeRatio ?? null, uncappedStats.sharpeRatio ?? null),
865
- sortinoRatio: calcDelta(baselineStats.sortinoRatio ?? null, uncappedStats.sortinoRatio ?? null),
895
+ sharpeRatio: calcDelta(
896
+ baselineStats.sharpeRatio ?? null,
897
+ uncappedStats.sharpeRatio ?? null,
898
+ ),
899
+ sortinoRatio: calcDelta(
900
+ baselineStats.sortinoRatio ?? null,
901
+ uncappedStats.sortinoRatio ?? null,
902
+ ),
866
903
  maxDrawdown: calcDelta(baselineStats.maxDrawdown, uncappedStats.maxDrawdown),
867
904
  netPl: calcDelta(baselineStats.netPl, uncappedStats.netPl),
868
- totalTrades: { original: baselineStats.totalTrades, scaled: uncappedStats.totalTrades },
905
+ totalTrades: {
906
+ original: baselineStats.totalTrades,
907
+ scaled: uncappedStats.totalTrades,
908
+ },
869
909
  };
870
910
  }
871
911
 
@@ -873,16 +913,13 @@ export function registerSimilarityBlockTools(
873
913
  const comparison = {
874
914
  sharpeRatio: calcDelta(
875
915
  baselineStats.sharpeRatio ?? null,
876
- scaledStats.sharpeRatio ?? null
916
+ scaledStats.sharpeRatio ?? null,
877
917
  ),
878
918
  sortinoRatio: calcDelta(
879
919
  baselineStats.sortinoRatio ?? null,
880
- scaledStats.sortinoRatio ?? null
881
- ),
882
- maxDrawdown: calcDelta(
883
- baselineStats.maxDrawdown,
884
- scaledStats.maxDrawdown
920
+ scaledStats.sortinoRatio ?? null,
885
921
  ),
922
+ maxDrawdown: calcDelta(baselineStats.maxDrawdown, scaledStats.maxDrawdown),
886
923
  netPl: calcDelta(baselineStats.netPl, scaledStats.netPl),
887
924
  totalTrades: {
888
925
  original: baselineStats.totalTrades,
@@ -944,10 +981,9 @@ export function registerSimilarityBlockTools(
944
981
  }
945
982
 
946
983
  // Merge all strategy names (original + any from backtest substitution)
947
- const allStrategyNames = Array.from(new Set([
948
- ...allStrategies,
949
- ...Object.keys(originalByStrategy),
950
- ])).sort();
984
+ const allStrategyNames = Array.from(
985
+ new Set([...allStrategies, ...Object.keys(originalByStrategy)]),
986
+ ).sort();
951
987
 
952
988
  for (const strategy of allStrategyNames) {
953
989
  const weight = appliedWeights[strategy] ?? 1.0;
@@ -955,17 +991,12 @@ export function registerSimilarityBlockTools(
955
991
  const scaled = scaledByStrategy[strategy] ?? { trades: 0, netPl: 0 };
956
992
 
957
993
  const origContributionPct =
958
- totalOriginalPl !== 0
959
- ? (orig.netPl / Math.abs(totalOriginalPl)) * 100
960
- : 0;
994
+ totalOriginalPl !== 0 ? (orig.netPl / Math.abs(totalOriginalPl)) * 100 : 0;
961
995
  const scaledContributionPct =
962
- totalScaledPl !== 0
963
- ? (scaled.netPl / Math.abs(totalScaledPl)) * 100
964
- : 0;
996
+ totalScaledPl !== 0 ? (scaled.netPl / Math.abs(totalScaledPl)) * 100 : 0;
965
997
 
966
998
  const deltaNetPl = scaled.netPl - orig.netPl;
967
- const deltaNetPlPct =
968
- orig.netPl !== 0 ? (deltaNetPl / Math.abs(orig.netPl)) * 100 : 0;
999
+ const deltaNetPlPct = orig.netPl !== 0 ? (deltaNetPl / Math.abs(orig.netPl)) * 100 : 0;
969
1000
 
970
1001
  const lookup = profileLookups[strategy];
971
1002
  const breakdown: StrategyBreakdown = {
@@ -998,7 +1029,8 @@ export function registerSimilarityBlockTools(
998
1029
  // ANLYS-07: ignoreMarginReq flag
999
1030
  if (lookup?.profile?.ignoreMarginReq) {
1000
1031
  breakdown.ignoreMarginReq = true;
1001
- breakdown.marginNote = "Strategy ignores margin requirements. Scaled notional exposure shown but buying power impact not estimated.";
1032
+ breakdown.marginNote =
1033
+ "Strategy ignores margin requirements. Scaled notional exposure shown but buying power impact not estimated.";
1002
1034
  breakdown.scaledNotionalExposure = Math.abs(scaled.netPl);
1003
1035
  }
1004
1036
 
@@ -1021,21 +1053,22 @@ export function registerSimilarityBlockTools(
1021
1053
  start: startDate ?? null,
1022
1054
  end: endDate ?? null,
1023
1055
  },
1024
- unknownStrategies:
1025
- unknownStrategies.length > 0 ? unknownStrategies : undefined,
1056
+ unknownStrategies: unknownStrategies.length > 0 ? unknownStrategies : undefined,
1026
1057
  comparison,
1027
1058
  perStrategy,
1028
1059
  };
1029
1060
 
1030
1061
  // Add profile-aware metadata only when profiles were found
1031
- const anyProfileFound = Object.values(profileLookups).some(l => l.status === "found");
1062
+ const anyProfileFound = Object.values(profileLookups).some((l) => l.status === "found");
1032
1063
  if (anyProfileFound) {
1033
- structuredData.dataAvailability = Object.entries(profileLookups).map(([strategy, lookup]) => ({
1034
- strategy,
1035
- profileStatus: lookup.status,
1036
- profileBlockId: lookup.profile?.blockId,
1037
- dataSource: dataSourceMap[strategy],
1038
- }));
1064
+ structuredData.dataAvailability = Object.entries(profileLookups).map(
1065
+ ([strategy, lookup]) => ({
1066
+ strategy,
1067
+ profileStatus: lookup.status,
1068
+ profileBlockId: lookup.profile?.blockId,
1069
+ dataSource: dataSourceMap[strategy],
1070
+ }),
1071
+ );
1039
1072
  }
1040
1073
 
1041
1074
  if (backtestSubstitutions.length > 0) {
@@ -1058,7 +1091,7 @@ export function registerSimilarityBlockTools(
1058
1091
  isError: true,
1059
1092
  };
1060
1093
  }
1061
- }
1062
- )
1094
+ },
1095
+ ),
1063
1096
  );
1064
1097
  }