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
@@ -10,11 +10,7 @@
10
10
  import { z } from "zod";
11
11
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { loadBlock, loadReportingLog } from "../../utils/block-loader.ts";
13
- import {
14
- createToolOutput,
15
- formatPercent,
16
- formatRatio,
17
- } from "../../utils/output-formatter.ts";
13
+ import { createToolOutput, formatPercent, formatRatio } from "../../utils/output-formatter.ts";
18
14
  import {
19
15
  PortfolioStatsCalculator,
20
16
  calculateCorrelationMatrix,
@@ -28,10 +24,7 @@ import { withSyncedBlock } from "../middleware/sync-middleware.ts";
28
24
  import { getConnection } from "../../db/connection.ts";
29
25
  import { listProfiles } from "../../db/profile-schemas.ts";
30
26
  import { computeSliceStats, type SliceStats } from "../../utils/analysis-stats.ts";
31
- import {
32
- buildLookaheadFreeQuery,
33
- type MarketLookupKey,
34
- } from "../../utils/field-timing.ts";
27
+ import { buildLookaheadFreeQuery, type MarketLookupKey } from "../../utils/field-timing.ts";
35
28
  import {
36
29
  DEFAULT_MARKET_TICKER,
37
30
  marketTickerDateKey,
@@ -127,7 +120,7 @@ function resultToRecords(result: {
127
120
  }
128
121
 
129
122
  function recordsByTickerDate(
130
- records: Record<string, unknown>[]
123
+ records: Record<string, unknown>[],
131
124
  ): Map<string, Record<string, unknown>> {
132
125
  const mapped = new Map<string, Record<string, unknown>>();
133
126
  for (const record of records) {
@@ -179,10 +172,14 @@ function getDteBucket(legs: { expiry: string }[]): string {
179
172
  * Returns the set of covered day numbers (1-5), or null if no DOW filter exists.
180
173
  */
181
174
  function extractDowCoverage(
182
- entryFilters: { field: string; operator: string; value: string | number | (string | number)[] }[]
175
+ entryFilters: { field: string; operator: string; value: string | number | (string | number)[] }[],
183
176
  ): Set<number> | null {
184
177
  const dayNameToNum: Record<string, number> = {
185
- monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5,
178
+ monday: 1,
179
+ tuesday: 2,
180
+ wednesday: 3,
181
+ thursday: 4,
182
+ friday: 5,
186
183
  };
187
184
 
188
185
  for (const filter of entryFilters) {
@@ -227,10 +224,13 @@ interface ProfileSectionResult {
227
224
  async function buildRegimeCoverageSection(
228
225
  profiles: StrategyProfile[],
229
226
  blockTrades: Trade[],
230
- baseDir: string
227
+ baseDir: string,
231
228
  ): Promise<ProfileSectionResult> {
232
229
  const flags: ProfileSectionResult["flags"] = [];
233
- const matrix: Record<string, Record<string, { expected: boolean; actual: SliceStats | null }>> = {};
230
+ const matrix: Record<
231
+ string,
232
+ Record<string, { expected: boolean; actual: SliceStats | null }>
233
+ > = {};
234
234
 
235
235
  // Collect all trades matched to market data
236
236
  const tradeKeys = uniqueTradeLookupKeys(blockTrades);
@@ -338,9 +338,7 @@ async function buildRegimeCoverageSection(
338
338
  /**
339
339
  * Section 2: Day-of-Week Coverage Heatmap
340
340
  */
341
- function buildDayCoverageSection(
342
- profiles: StrategyProfile[]
343
- ): ProfileSectionResult {
341
+ function buildDayCoverageSection(profiles: StrategyProfile[]): ProfileSectionResult {
344
342
  const flags: ProfileSectionResult["flags"] = [];
345
343
  const heatmap: Record<string, Record<string, "covered" | "not_covered" | "no_filter">> = {};
346
344
  const coveredDays = new Set<number>();
@@ -390,27 +388,28 @@ function buildDayCoverageSection(
390
388
  /**
391
389
  * Section 3: Allocation Concentration
392
390
  */
393
- function buildConcentrationSection(
394
- profiles: StrategyProfile[]
395
- ): ProfileSectionResult {
391
+ function buildConcentrationSection(profiles: StrategyProfile[]): ProfileSectionResult {
396
392
  const flags: ProfileSectionResult["flags"] = [];
397
393
 
398
394
  // Determine allocation weights per strategy
399
- const allocations: { name: string; pct: number; structureType: string; underlying: string; dteBucket: string }[] = [];
395
+ const allocations: {
396
+ name: string;
397
+ pct: number;
398
+ structureType: string;
399
+ underlying: string;
400
+ dteBucket: string;
401
+ }[] = [];
400
402
  let hasAllocationData = false;
401
403
 
402
404
  const totalProfiles = profiles.length;
403
405
  for (const p of profiles) {
404
- const pct =
405
- p.positionSizing?.backtestAllocationPct ??
406
- p.positionSizing?.allocationPct ??
407
- null;
406
+ const pct = p.positionSizing?.backtestAllocationPct ?? p.positionSizing?.allocationPct ?? null;
408
407
 
409
408
  if (pct !== null) hasAllocationData = true;
410
409
 
411
410
  allocations.push({
412
411
  name: p.strategyName,
413
- pct: pct ?? (100 / totalProfiles), // Equal weight fallback
412
+ pct: pct ?? 100 / totalProfiles, // Equal weight fallback
414
413
  structureType: p.structureType || "unspecified",
415
414
  underlying: p.underlying || "unspecified",
416
415
  dteBucket: getDteBucket(p.legs),
@@ -426,14 +425,15 @@ function buildConcentrationSection(
426
425
 
427
426
  // Group by each dimension
428
427
  function groupBy(
429
- key: "structureType" | "underlying" | "dteBucket"
428
+ key: "structureType" | "underlying" | "dteBucket",
430
429
  ): Record<string, { strategies: string[]; allocationPct: number }> {
431
430
  const groups: Record<string, { strategies: string[]; allocationPct: number }> = {};
432
431
  for (const a of normalizedAllocations) {
433
432
  const category = a[key];
434
433
  if (!groups[category]) groups[category] = { strategies: [], allocationPct: 0 };
435
434
  groups[category].strategies.push(a.name);
436
- groups[category].allocationPct = Math.round((groups[category].allocationPct + a.pct) * 100) / 100;
435
+ groups[category].allocationPct =
436
+ Math.round((groups[category].allocationPct + a.pct) * 100) / 100;
437
437
  }
438
438
  return groups;
439
439
  }
@@ -463,8 +463,14 @@ function buildConcentrationSection(
463
463
  }
464
464
 
465
465
  // Neutral observations
466
- for (const [dim, groups] of Object.entries({ structure: byStructure, underlying: byUnderlying, dte: byDte })) {
467
- const topCategory = Object.entries(groups).sort((a, b) => b[1].allocationPct - a[1].allocationPct)[0];
466
+ for (const [dim, groups] of Object.entries({
467
+ structure: byStructure,
468
+ underlying: byUnderlying,
469
+ dte: byDte,
470
+ })) {
471
+ const topCategory = Object.entries(groups).sort(
472
+ (a, b) => b[1].allocationPct - a[1].allocationPct,
473
+ )[0];
468
474
  if (topCategory) {
469
475
  flags.push({
470
476
  type: "info",
@@ -496,19 +502,19 @@ function buildConcentrationSection(
496
502
  * Section 4: Correlation Risk Flags
497
503
  * Finds profile pairs sharing underlying + DTE bucket + entry days overlap.
498
504
  */
499
- function buildCorrelationRiskSection(
500
- profiles: StrategyProfile[]
501
- ): ProfileSectionResult {
505
+ function buildCorrelationRiskSection(profiles: StrategyProfile[]): ProfileSectionResult {
502
506
  const flags: ProfileSectionResult["flags"] = [];
503
507
 
504
508
  if (profiles.length < 2) {
505
509
  return {
506
510
  grade: null,
507
- flags: [{
508
- type: "info",
509
- dimension: "correlationRisk",
510
- message: "Skipped: need at least 2 profiles for correlation risk analysis",
511
- }],
511
+ flags: [
512
+ {
513
+ type: "info",
514
+ dimension: "correlationRisk",
515
+ message: "Skipped: need at least 2 profiles for correlation risk analysis",
516
+ },
517
+ ],
512
518
  data: {},
513
519
  keyNumbers: {},
514
520
  };
@@ -576,7 +582,8 @@ function buildCorrelationRiskSection(
576
582
  flags.push({
577
583
  type: "info",
578
584
  dimension: "correlationRisk",
579
- message: "No strategy pairs share all three: same underlying, same DTE bucket, overlapping entry days",
585
+ message:
586
+ "No strategy pairs share all three: same underlying, same DTE bucket, overlapping entry days",
580
587
  });
581
588
  }
582
589
 
@@ -595,15 +602,33 @@ async function buildScalingSection(
595
602
  profiles: StrategyProfile[],
596
603
  blockTrades: Trade[],
597
604
  baseDir: string,
598
- blockId: string
605
+ blockId: string,
599
606
  ): Promise<ProfileSectionResult> {
600
607
  const flags: ProfileSectionResult["flags"] = [];
601
- const scalingData: Record<string, {
602
- standalone: { tradeCount: number; netPl: number; avgPlPerTrade: number; avgPlPerContract: number } | null;
603
- portfolioBacktest: { tradeCount: number; netPl: number; avgPlPerTrade: number; avgPlPerContract: number } | null;
604
- liveReporting: { tradeCount: number; netPl: number; avgPlPerTrade: number; avgPlPerContract: number } | null;
605
- sizingNotes: string[];
606
- }> = {};
608
+ const scalingData: Record<
609
+ string,
610
+ {
611
+ standalone: {
612
+ tradeCount: number;
613
+ netPl: number;
614
+ avgPlPerTrade: number;
615
+ avgPlPerContract: number;
616
+ } | null;
617
+ portfolioBacktest: {
618
+ tradeCount: number;
619
+ netPl: number;
620
+ avgPlPerTrade: number;
621
+ avgPlPerContract: number;
622
+ } | null;
623
+ liveReporting: {
624
+ tradeCount: number;
625
+ netPl: number;
626
+ avgPlPerTrade: number;
627
+ avgPlPerContract: number;
628
+ } | null;
629
+ sizingNotes: string[];
630
+ }
631
+ > = {};
607
632
 
608
633
  // Try to load reporting log for live data
609
634
  let reportingTrades: Array<{ strategy: string; pl: number; numContracts: number }> = [];
@@ -622,7 +647,7 @@ async function buildScalingSection(
622
647
  const deviations: number[] = [];
623
648
 
624
649
  for (const profile of profiles) {
625
- const entry: typeof scalingData[string] = {
650
+ const entry: (typeof scalingData)[string] = {
626
651
  standalone: null,
627
652
  portfolioBacktest: null,
628
653
  liveReporting: null,
@@ -645,7 +670,8 @@ async function buildScalingSection(
645
670
  tradeCount: standaloneTrades.length,
646
671
  netPl: Math.round(totalPl * 100) / 100,
647
672
  avgPlPerTrade: Math.round((totalPl / standaloneTrades.length) * 100) / 100,
648
- avgPlPerContract: totalContracts > 0 ? Math.round((totalPl / totalContracts) * 100) / 100 : 0,
673
+ avgPlPerContract:
674
+ totalContracts > 0 ? Math.round((totalPl / totalContracts) * 100) / 100 : 0,
649
675
  };
650
676
  }
651
677
  } catch {
@@ -666,13 +692,14 @@ async function buildScalingSection(
666
692
  tradeCount: portfolioTrades.length,
667
693
  netPl: Math.round(totalPl * 100) / 100,
668
694
  avgPlPerTrade: Math.round((totalPl / portfolioTrades.length) * 100) / 100,
669
- avgPlPerContract: totalContracts > 0 ? Math.round((totalPl / totalContracts) * 100) / 100 : 0,
695
+ avgPlPerContract:
696
+ totalContracts > 0 ? Math.round((totalPl / totalContracts) * 100) / 100 : 0,
670
697
  };
671
698
  }
672
699
 
673
700
  // 3. Live reporting log
674
701
  const liveFiltered = reportingTrades.filter(
675
- (t) => t.strategy.toLowerCase() === profile.strategyName.toLowerCase()
702
+ (t) => t.strategy.toLowerCase() === profile.strategyName.toLowerCase(),
676
703
  );
677
704
  if (liveFiltered.length > 0) {
678
705
  hasLiveData = true;
@@ -682,14 +709,15 @@ async function buildScalingSection(
682
709
  tradeCount: liveFiltered.length,
683
710
  netPl: Math.round(totalPl * 100) / 100,
684
711
  avgPlPerTrade: Math.round((totalPl / liveFiltered.length) * 100) / 100,
685
- avgPlPerContract: totalContracts > 0 ? Math.round((totalPl / totalContracts) * 100) / 100 : 0,
712
+ avgPlPerContract:
713
+ totalContracts > 0 ? Math.round((totalPl / totalContracts) * 100) / 100 : 0,
686
714
  };
687
715
 
688
716
  // Compute per-contract deviation if we have backtest reference
689
717
  const btRef = entry.portfolioBacktest ?? entry.standalone;
690
718
  if (btRef && btRef.avgPlPerContract !== 0 && entry.liveReporting.avgPlPerContract !== 0) {
691
719
  const deviation = Math.abs(
692
- (entry.liveReporting.avgPlPerContract - btRef.avgPlPerContract) / btRef.avgPlPerContract
720
+ (entry.liveReporting.avgPlPerContract - btRef.avgPlPerContract) / btRef.avgPlPerContract,
693
721
  );
694
722
  deviations.push(deviation);
695
723
  }
@@ -698,9 +726,13 @@ async function buildScalingSection(
698
726
  // Sizing notes
699
727
  if (profile.positionSizing) {
700
728
  const ps = profile.positionSizing;
701
- if (ps.backtestAllocationPct && ps.liveAllocationPct && ps.backtestAllocationPct !== ps.liveAllocationPct) {
729
+ if (
730
+ ps.backtestAllocationPct &&
731
+ ps.liveAllocationPct &&
732
+ ps.backtestAllocationPct !== ps.liveAllocationPct
733
+ ) {
702
734
  entry.sizingNotes.push(
703
- `Backtest allocation ${ps.backtestAllocationPct}% vs live ${ps.liveAllocationPct}%`
735
+ `Backtest allocation ${ps.backtestAllocationPct}% vs live ${ps.liveAllocationPct}%`,
704
736
  );
705
737
  }
706
738
  entry.sizingNotes.push(`Sizing method: ${ps.method}`);
@@ -752,10 +784,7 @@ async function buildScalingSection(
752
784
  /**
753
785
  * Register health block tools
754
786
  */
755
- export function registerHealthBlockTools(
756
- server: McpServer,
757
- baseDir: string
758
- ): void {
787
+ export function registerHealthBlockTools(server: McpServer, baseDir: string): void {
759
788
  const calculator = new PortfolioStatsCalculator();
760
789
 
761
790
  // Tool 13: portfolio_health_check
@@ -772,7 +801,7 @@ export function registerHealthBlockTools(
772
801
  .max(1)
773
802
  .default(HEALTH_CHECK_DEFAULTS.correlationThreshold)
774
803
  .describe(
775
- `Flag correlation pairs above this (default: ${HEALTH_CHECK_DEFAULTS.correlationThreshold})`
804
+ `Flag correlation pairs above this (default: ${HEALTH_CHECK_DEFAULTS.correlationThreshold})`,
776
805
  ),
777
806
  tailDependenceThreshold: z
778
807
  .number()
@@ -780,7 +809,7 @@ export function registerHealthBlockTools(
780
809
  .max(1)
781
810
  .default(HEALTH_CHECK_DEFAULTS.tailDependenceThreshold)
782
811
  .describe(
783
- `Flag tail dependence pairs above this (default: ${HEALTH_CHECK_DEFAULTS.tailDependenceThreshold})`
812
+ `Flag tail dependence pairs above this (default: ${HEALTH_CHECK_DEFAULTS.tailDependenceThreshold})`,
784
813
  ),
785
814
  profitProbabilityThreshold: z
786
815
  .number()
@@ -788,20 +817,20 @@ export function registerHealthBlockTools(
788
817
  .max(1)
789
818
  .default(HEALTH_CHECK_DEFAULTS.profitProbabilityThreshold)
790
819
  .describe(
791
- `Monte Carlo profit probability warning threshold (default: ${HEALTH_CHECK_DEFAULTS.profitProbabilityThreshold})`
820
+ `Monte Carlo profit probability warning threshold (default: ${HEALTH_CHECK_DEFAULTS.profitProbabilityThreshold})`,
792
821
  ),
793
822
  wfeThreshold: z
794
823
  .number()
795
824
  .default(HEALTH_CHECK_DEFAULTS.wfeThreshold)
796
825
  .describe(
797
- `Walk-forward efficiency warning threshold (default: ${HEALTH_CHECK_DEFAULTS.wfeThreshold})`
826
+ `Walk-forward efficiency warning threshold (default: ${HEALTH_CHECK_DEFAULTS.wfeThreshold})`,
798
827
  ),
799
828
  mddMultiplierThreshold: z
800
829
  .number()
801
830
  .min(1)
802
831
  .default(HEALTH_CHECK_DEFAULTS.mddMultiplierThreshold)
803
832
  .describe(
804
- `MC median MDD vs historical MDD multiplier warning threshold (default: ${HEALTH_CHECK_DEFAULTS.mddMultiplierThreshold})`
833
+ `MC median MDD vs historical MDD multiplier warning threshold (default: ${HEALTH_CHECK_DEFAULTS.mddMultiplierThreshold})`,
805
834
  ),
806
835
  }),
807
836
  },
@@ -816,18 +845,14 @@ export function registerHealthBlockTools(
816
845
  mddMultiplierThreshold,
817
846
  }) => {
818
847
  // Apply defaults for optional parameters
819
- const corrThreshold =
820
- correlationThreshold ?? HEALTH_CHECK_DEFAULTS.correlationThreshold;
848
+ const corrThreshold = correlationThreshold ?? HEALTH_CHECK_DEFAULTS.correlationThreshold;
821
849
  const tailThreshold =
822
- tailDependenceThreshold ??
823
- HEALTH_CHECK_DEFAULTS.tailDependenceThreshold;
850
+ tailDependenceThreshold ?? HEALTH_CHECK_DEFAULTS.tailDependenceThreshold;
824
851
  const profitThreshold =
825
- profitProbabilityThreshold ??
826
- HEALTH_CHECK_DEFAULTS.profitProbabilityThreshold;
852
+ profitProbabilityThreshold ?? HEALTH_CHECK_DEFAULTS.profitProbabilityThreshold;
827
853
  const wfeThresh = wfeThreshold ?? HEALTH_CHECK_DEFAULTS.wfeThreshold;
828
854
  const mddMultThresh =
829
- mddMultiplierThreshold ??
830
- HEALTH_CHECK_DEFAULTS.mddMultiplierThreshold;
855
+ mddMultiplierThreshold ?? HEALTH_CHECK_DEFAULTS.mddMultiplierThreshold;
831
856
 
832
857
  try {
833
858
  const block = await loadBlock(baseDir, blockId);
@@ -845,12 +870,10 @@ export function registerHealthBlockTools(
845
870
  };
846
871
  }
847
872
 
848
- // Get unique strategies
849
- const strategies = Array.from(
850
- new Set(trades.map((t) => t.strategy))
851
- ).sort();
873
+ // Get unique strategies
874
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
852
875
 
853
- // Require at least 2 strategies and 20 trades
876
+ // Require at least 2 strategies and 20 trades
854
877
  if (strategies.length < 2) {
855
878
  return {
856
879
  content: [
@@ -875,545 +898,530 @@ export function registerHealthBlockTools(
875
898
  };
876
899
  }
877
900
 
878
- // Calculate portfolio stats
879
- const stats = calculator.calculatePortfolioStats(
880
- trades,
881
- undefined, // No daily logs per Phase 17 constraint
882
- true // Force trade-based calculations
883
- );
901
+ // Calculate portfolio stats
902
+ const stats = calculator.calculatePortfolioStats(
903
+ trades,
904
+ undefined, // No daily logs per Phase 17 constraint
905
+ true, // Force trade-based calculations
906
+ );
884
907
 
885
- // Calculate correlation matrix (kendall, raw, opened)
886
- const correlationMatrix = calculateCorrelationMatrix(trades, {
887
- method: "kendall",
888
- normalization: "raw",
889
- dateBasis: "opened",
890
- alignment: "shared",
891
- });
892
-
893
- // Calculate tail risk (0.1 threshold)
894
- const tailRisk = performTailRiskAnalysis(trades, {
895
- tailThreshold: 0.1,
896
- normalization: "raw",
897
- dateBasis: "opened",
898
- minTradingDays: 10, // Lower requirement for health check
899
- });
900
-
901
- // Run Monte Carlo (1000 sims, trades method)
902
- const sortedTrades = [...trades].sort(
903
- (a, b) =>
904
- new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
905
- );
906
- const firstTrade = sortedTrades[0];
907
- const lastTrade = sortedTrades[sortedTrades.length - 1];
908
- const inferredCapital = firstTrade.fundsAtClose - firstTrade.pl;
909
- const initialCapital = inferredCapital > 0 ? inferredCapital : 100000;
910
- const daySpan =
911
- (new Date(lastTrade.dateOpened).getTime() -
912
- new Date(firstTrade.dateOpened).getTime()) /
913
- (24 * 60 * 60 * 1000);
914
- const calculatedTradesPerYear =
915
- daySpan > 0 ? (trades.length / daySpan) * 365 : 252;
916
-
917
- const mcParams: MonteCarloParams = {
918
- numSimulations: 1000,
919
- simulationLength: trades.length,
920
- resampleMethod: "trades",
921
- initialCapital,
922
- tradesPerYear: calculatedTradesPerYear,
923
- worstCaseEnabled: true,
924
- worstCasePercentage: 5,
925
- worstCaseMode: "pool",
926
- worstCaseBasedOn: "simulation",
927
- worstCaseSizing: "relative",
928
- };
929
-
930
- const mcResult = runMonteCarloSimulation(trades, mcParams);
931
- const mcStats = mcResult.statistics;
932
-
933
- // Run a second MC with percentage-based resampling to detect position sizing inflation
934
- const mcPctParams: MonteCarloParams = {
935
- ...mcParams,
936
- resampleMethod: "percentage",
937
- worstCaseEnabled: false, // Not needed for comparison
938
- };
939
- const mcPctResult = runMonteCarloSimulation(trades, mcPctParams);
940
- const mcPctStats = mcPctResult.statistics;
941
-
942
- // Detect percentage-based position sizing from profiles
943
- let useNormalization = false;
944
- let profiles: StrategyProfile[] = [];
945
- try {
946
- const conn = await getConnection(baseDir);
947
- profiles = await listProfiles(conn, blockId, baseDir);
948
- useNormalization = profiles.some(
949
- (p) => p.positionSizing?.method === "pct_of_portfolio"
908
+ // Calculate correlation matrix (kendall, raw, opened)
909
+ const correlationMatrix = calculateCorrelationMatrix(trades, {
910
+ method: "kendall",
911
+ normalization: "raw",
912
+ dateBasis: "opened",
913
+ alignment: "shared",
914
+ });
915
+
916
+ // Calculate tail risk (0.1 threshold)
917
+ const tailRisk = performTailRiskAnalysis(trades, {
918
+ tailThreshold: 0.1,
919
+ normalization: "raw",
920
+ dateBasis: "opened",
921
+ minTradingDays: 10, // Lower requirement for health check
922
+ });
923
+
924
+ // Run Monte Carlo (1000 sims, trades method)
925
+ const sortedTrades = [...trades].sort(
926
+ (a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
950
927
  );
951
- } catch {
952
- // Profile lookup is best-effort; default to no normalization
953
- }
928
+ const firstTrade = sortedTrades[0];
929
+ const lastTrade = sortedTrades[sortedTrades.length - 1];
930
+ const inferredCapital = firstTrade.fundsAtClose - firstTrade.pl;
931
+ const initialCapital = inferredCapital > 0 ? inferredCapital : 100000;
932
+ const daySpan =
933
+ (new Date(lastTrade.dateOpened).getTime() - new Date(firstTrade.dateOpened).getTime()) /
934
+ (24 * 60 * 60 * 1000);
935
+ const calculatedTradesPerYear = daySpan > 0 ? (trades.length / daySpan) * 365 : 252;
936
+
937
+ const mcParams: MonteCarloParams = {
938
+ numSimulations: 1000,
939
+ simulationLength: trades.length,
940
+ resampleMethod: "trades",
941
+ initialCapital,
942
+ tradesPerYear: calculatedTradesPerYear,
943
+ worstCaseEnabled: true,
944
+ worstCasePercentage: 5,
945
+ worstCaseMode: "pool",
946
+ worstCaseBasedOn: "simulation",
947
+ worstCaseSizing: "relative",
948
+ };
954
949
 
955
- // Run WFD (walk-forward degradation) with weighted efficiency
956
- let wfeResult: number | null = null;
957
- let wfaSkipped = false;
958
- try {
959
- if (trades.length >= 20) {
960
- const wfTrades = useNormalization ? normalizeToOneLot(trades) : trades;
961
- const wfdResult = analyzeWalkForwardDegradation(wfTrades, {
962
- normalizeTo1Lot: false, // Already normalized above if needed
963
- weightByTradeCount: true,
964
- minOosFraction: 0.5,
965
- });
966
- wfeResult = wfdResult.weightedOverallEfficiency.sharpe;
967
- } else {
968
- wfaSkipped = true;
950
+ const mcResult = runMonteCarloSimulation(trades, mcParams);
951
+ const mcStats = mcResult.statistics;
952
+
953
+ // Run a second MC with percentage-based resampling to detect position sizing inflation
954
+ const mcPctParams: MonteCarloParams = {
955
+ ...mcParams,
956
+ resampleMethod: "percentage",
957
+ worstCaseEnabled: false, // Not needed for comparison
958
+ };
959
+ const mcPctResult = runMonteCarloSimulation(trades, mcPctParams);
960
+ const mcPctStats = mcPctResult.statistics;
961
+
962
+ // Detect percentage-based position sizing from profiles
963
+ let useNormalization = false;
964
+ let profiles: StrategyProfile[] = [];
965
+ try {
966
+ const conn = await getConnection(baseDir);
967
+ profiles = await listProfiles(conn, blockId, baseDir);
968
+ useNormalization = profiles.some(
969
+ (p) => p.positionSizing?.method === "pct_of_portfolio",
970
+ );
971
+ } catch {
972
+ // Profile lookup is best-effort; default to no normalization
969
973
  }
970
- } catch {
971
- wfaSkipped = true;
972
- }
973
974
 
974
- // Calculate average correlation and tail dependence
975
- let totalCorrelation = 0;
976
- let correlationCount = 0;
977
- for (let i = 0; i < correlationMatrix.strategies.length; i++) {
978
- for (let j = i + 1; j < correlationMatrix.strategies.length; j++) {
979
- const val = correlationMatrix.correlationData[i][j];
980
- if (!Number.isNaN(val) && val !== null) {
981
- totalCorrelation += Math.abs(val);
982
- correlationCount++;
975
+ // Run WFD (walk-forward degradation) with weighted efficiency
976
+ let wfeResult: number | null = null;
977
+ let wfaSkipped = false;
978
+ try {
979
+ if (trades.length >= 20) {
980
+ const wfTrades = useNormalization ? normalizeToOneLot(trades) : trades;
981
+ const wfdResult = analyzeWalkForwardDegradation(wfTrades, {
982
+ normalizeTo1Lot: false, // Already normalized above if needed
983
+ weightByTradeCount: true,
984
+ minOosFraction: 0.5,
985
+ });
986
+ wfeResult = wfdResult.weightedOverallEfficiency.sharpe;
987
+ } else {
988
+ wfaSkipped = true;
983
989
  }
990
+ } catch {
991
+ wfaSkipped = true;
984
992
  }
985
- }
986
- const avgCorrelation =
987
- correlationCount > 0 ? totalCorrelation / correlationCount : 0;
988
-
989
- let totalTailDependence = 0;
990
- let tailCount = 0;
991
- for (let i = 0; i < tailRisk.strategies.length; i++) {
992
- for (let j = i + 1; j < tailRisk.strategies.length; j++) {
993
- const valAB = tailRisk.jointTailRiskMatrix[i]?.[j];
994
- const valBA = tailRisk.jointTailRiskMatrix[j]?.[i];
995
- if (
996
- valAB !== undefined &&
997
- valBA !== undefined &&
998
- !Number.isNaN(valAB) &&
999
- !Number.isNaN(valBA)
1000
- ) {
1001
- totalTailDependence += (valAB + valBA) / 2;
1002
- tailCount++;
993
+
994
+ // Calculate average correlation and tail dependence
995
+ let totalCorrelation = 0;
996
+ let correlationCount = 0;
997
+ for (let i = 0; i < correlationMatrix.strategies.length; i++) {
998
+ for (let j = i + 1; j < correlationMatrix.strategies.length; j++) {
999
+ const val = correlationMatrix.correlationData[i][j];
1000
+ if (!Number.isNaN(val) && val !== null) {
1001
+ totalCorrelation += Math.abs(val);
1002
+ correlationCount++;
1003
+ }
1003
1004
  }
1004
1005
  }
1005
- }
1006
- const avgTailDependence =
1007
- tailCount > 0 ? totalTailDependence / tailCount : 0;
1008
-
1009
- // Build flags array (widened dimension type to include new sections)
1010
- type Flag = {
1011
- type: "warning" | "pass" | "info";
1012
- dimension: string;
1013
- message: string;
1014
- };
1015
- const flags: Flag[] = [];
1016
-
1017
- // High correlation pairs
1018
- const highCorrPairs: string[] = [];
1019
- for (let i = 0; i < correlationMatrix.strategies.length; i++) {
1020
- for (let j = i + 1; j < correlationMatrix.strategies.length; j++) {
1021
- const val = correlationMatrix.correlationData[i][j];
1022
- const sampleSize = correlationMatrix.sampleSizes[i][j];
1023
- if (!Number.isNaN(val) && Math.abs(val) > corrThreshold && sampleSize >= 10) {
1024
- highCorrPairs.push(
1025
- `${correlationMatrix.strategies[i]} & ${correlationMatrix.strategies[j]} (${val.toFixed(2)}, n=${sampleSize})`
1026
- );
1006
+ const avgCorrelation = correlationCount > 0 ? totalCorrelation / correlationCount : 0;
1007
+
1008
+ let totalTailDependence = 0;
1009
+ let tailCount = 0;
1010
+ for (let i = 0; i < tailRisk.strategies.length; i++) {
1011
+ for (let j = i + 1; j < tailRisk.strategies.length; j++) {
1012
+ const valAB = tailRisk.jointTailRiskMatrix[i]?.[j];
1013
+ const valBA = tailRisk.jointTailRiskMatrix[j]?.[i];
1014
+ if (
1015
+ valAB !== undefined &&
1016
+ valBA !== undefined &&
1017
+ !Number.isNaN(valAB) &&
1018
+ !Number.isNaN(valBA)
1019
+ ) {
1020
+ totalTailDependence += (valAB + valBA) / 2;
1021
+ tailCount++;
1022
+ }
1027
1023
  }
1028
1024
  }
1029
- }
1030
- if (highCorrPairs.length > 0) {
1031
- flags.push({
1032
- type: "warning",
1033
- dimension: "diversification",
1034
- message: `High correlation pairs (>${corrThreshold}): ${highCorrPairs.join(", ")}`,
1035
- });
1036
- } else {
1037
- flags.push({
1038
- type: "pass",
1039
- dimension: "diversification",
1040
- message: `No correlation pairs above ${corrThreshold} threshold`,
1041
- });
1042
- }
1025
+ const avgTailDependence = tailCount > 0 ? totalTailDependence / tailCount : 0;
1043
1026
 
1044
- // High tail dependence pairs
1045
- // Build strategy-to-correlation-index map for per-pair sample sizes
1046
- const corrStrategyIndex = new Map<string, number>();
1047
- correlationMatrix.strategies.forEach((s, i) =>
1048
- corrStrategyIndex.set(s, i)
1049
- );
1050
-
1051
- const highTailPairs: string[] = [];
1052
- for (let i = 0; i < tailRisk.strategies.length; i++) {
1053
- for (let j = i + 1; j < tailRisk.strategies.length; j++) {
1054
- const valAB = tailRisk.jointTailRiskMatrix[i]?.[j];
1055
- const valBA = tailRisk.jointTailRiskMatrix[j]?.[i];
1056
- if (
1057
- valAB !== undefined &&
1058
- valBA !== undefined &&
1059
- !Number.isNaN(valAB) &&
1060
- !Number.isNaN(valBA)
1061
- ) {
1062
- const avgTail = (valAB + valBA) / 2;
1063
- // Look up per-pair sample size from correlation matrix
1064
- const corrI = corrStrategyIndex.get(tailRisk.strategies[i]);
1065
- const corrJ = corrStrategyIndex.get(tailRisk.strategies[j]);
1066
- const pairSampleSize =
1067
- corrI !== undefined && corrJ !== undefined
1068
- ? correlationMatrix.sampleSizes[corrI][corrJ]
1069
- : null;
1070
- if (avgTail > tailThreshold && pairSampleSize !== null && pairSampleSize >= 10) {
1071
- highTailPairs.push(
1072
- `${tailRisk.strategies[i]} & ${tailRisk.strategies[j]} (${avgTail.toFixed(2)}${pairSampleSize !== null ? `, n=${pairSampleSize}` : ""})`
1027
+ // Build flags array (widened dimension type to include new sections)
1028
+ type Flag = {
1029
+ type: "warning" | "pass" | "info";
1030
+ dimension: string;
1031
+ message: string;
1032
+ };
1033
+ const flags: Flag[] = [];
1034
+
1035
+ // High correlation pairs
1036
+ const highCorrPairs: string[] = [];
1037
+ for (let i = 0; i < correlationMatrix.strategies.length; i++) {
1038
+ for (let j = i + 1; j < correlationMatrix.strategies.length; j++) {
1039
+ const val = correlationMatrix.correlationData[i][j];
1040
+ const sampleSize = correlationMatrix.sampleSizes[i][j];
1041
+ if (!Number.isNaN(val) && Math.abs(val) > corrThreshold && sampleSize >= 10) {
1042
+ highCorrPairs.push(
1043
+ `${correlationMatrix.strategies[i]} & ${correlationMatrix.strategies[j]} (${val.toFixed(2)}, n=${sampleSize})`,
1073
1044
  );
1074
1045
  }
1075
1046
  }
1076
1047
  }
1077
- }
1078
- if (highTailPairs.length > 0) {
1079
- flags.push({
1080
- type: "warning",
1081
- dimension: "tailRisk",
1082
- message: `High tail dependence pairs (>${tailThreshold}): ${highTailPairs.join(", ")}`,
1083
- });
1084
- } else {
1085
- flags.push({
1086
- type: "pass",
1087
- dimension: "tailRisk",
1088
- message: `No tail dependence pairs above ${tailThreshold} threshold`,
1089
- });
1090
- }
1091
-
1092
- // MC profit probability below threshold
1093
- if (mcStats.probabilityOfProfit < profitThreshold) {
1094
- flags.push({
1095
- type: "warning",
1096
- dimension: "consistency",
1097
- message: `Monte Carlo profit probability (${formatPercent(mcStats.probabilityOfProfit * 100)}) below ${formatPercent(profitThreshold * 100)} threshold`,
1098
- });
1099
- } else {
1100
- flags.push({
1101
- type: "pass",
1102
- dimension: "consistency",
1103
- message: `Monte Carlo profit probability (${formatPercent(mcStats.probabilityOfProfit * 100)}) meets ${formatPercent(profitThreshold * 100)} threshold`,
1104
- });
1105
- }
1048
+ if (highCorrPairs.length > 0) {
1049
+ flags.push({
1050
+ type: "warning",
1051
+ dimension: "diversification",
1052
+ message: `High correlation pairs (>${corrThreshold}): ${highCorrPairs.join(", ")}`,
1053
+ });
1054
+ } else {
1055
+ flags.push({
1056
+ type: "pass",
1057
+ dimension: "diversification",
1058
+ message: `No correlation pairs above ${corrThreshold} threshold`,
1059
+ });
1060
+ }
1106
1061
 
1107
- // MC median MDD vs historical MDD multiplier
1108
- // mcStats.medianMaxDrawdown is a decimal (0.12 = 12%)
1109
- // stats.maxDrawdown is a percentage (12 = 12%)
1110
- // Convert stats.maxDrawdown to decimal for comparison
1111
- const historicalMddDecimal = stats.maxDrawdown / 100;
1112
- const mcMddMultiplier =
1113
- historicalMddDecimal > 0
1114
- ? mcStats.medianMaxDrawdown / historicalMddDecimal
1115
- : null;
1116
- const mcPctMddMultiplier =
1117
- historicalMddDecimal > 0
1118
- ? mcPctStats.medianMaxDrawdown / historicalMddDecimal
1119
- : null;
1120
-
1121
- // Detect position sizing inflation: dollar-mode MDD much higher than percentage-mode
1122
- const sizingInflated =
1123
- mcMddMultiplier !== null &&
1124
- mcPctMddMultiplier !== null &&
1125
- mcMddMultiplier > 2 * mcPctMddMultiplier;
1126
-
1127
- if (sizingInflated) {
1128
- // Dollar-mode MDD is inflated by position sizing growth — report both
1129
- const pctExceeds = mcPctMddMultiplier! > mddMultThresh;
1130
- flags.push({
1131
- type: pctExceeds ? "warning" : "info",
1132
- dimension: "consistency",
1133
- message: `Monte Carlo MDD: dollar-mode ${formatPercent(mcStats.medianMaxDrawdown * 100)} (${mcMddMultiplier!.toFixed(1)}x historical) is inflated by position sizing growth. Percentage-mode ${formatPercent(mcPctStats.medianMaxDrawdown * 100)} (${mcPctMddMultiplier!.toFixed(1)}x historical) is more representative for % scaling portfolios`,
1134
- });
1135
- } else if (mcMddMultiplier !== null && mcMddMultiplier > mddMultThresh) {
1136
- flags.push({
1137
- type: "warning",
1138
- dimension: "consistency",
1139
- message: `Monte Carlo median MDD (${formatPercent(mcStats.medianMaxDrawdown * 100)}) is ${mcMddMultiplier.toFixed(1)}x historical MDD (${formatPercent(stats.maxDrawdown)}) - exceeds ${mddMultThresh}x threshold`,
1140
- });
1141
- } else if (mcMddMultiplier !== null) {
1142
- flags.push({
1143
- type: "pass",
1144
- dimension: "consistency",
1145
- message: `Monte Carlo median MDD (${formatPercent(mcStats.medianMaxDrawdown * 100)}) is ${mcMddMultiplier.toFixed(1)}x historical MDD - within ${mddMultThresh}x threshold`,
1146
- });
1147
- }
1062
+ // High tail dependence pairs
1063
+ // Build strategy-to-correlation-index map for per-pair sample sizes
1064
+ const corrStrategyIndex = new Map<string, number>();
1065
+ correlationMatrix.strategies.forEach((s, i) => corrStrategyIndex.set(s, i));
1066
+
1067
+ const highTailPairs: string[] = [];
1068
+ for (let i = 0; i < tailRisk.strategies.length; i++) {
1069
+ for (let j = i + 1; j < tailRisk.strategies.length; j++) {
1070
+ const valAB = tailRisk.jointTailRiskMatrix[i]?.[j];
1071
+ const valBA = tailRisk.jointTailRiskMatrix[j]?.[i];
1072
+ if (
1073
+ valAB !== undefined &&
1074
+ valBA !== undefined &&
1075
+ !Number.isNaN(valAB) &&
1076
+ !Number.isNaN(valBA)
1077
+ ) {
1078
+ const avgTail = (valAB + valBA) / 2;
1079
+ // Look up per-pair sample size from correlation matrix
1080
+ const corrI = corrStrategyIndex.get(tailRisk.strategies[i]);
1081
+ const corrJ = corrStrategyIndex.get(tailRisk.strategies[j]);
1082
+ const pairSampleSize =
1083
+ corrI !== undefined && corrJ !== undefined
1084
+ ? correlationMatrix.sampleSizes[corrI][corrJ]
1085
+ : null;
1086
+ if (avgTail > tailThreshold && pairSampleSize !== null && pairSampleSize >= 10) {
1087
+ highTailPairs.push(
1088
+ `${tailRisk.strategies[i]} & ${tailRisk.strategies[j]} (${avgTail.toFixed(2)}${pairSampleSize !== null ? `, n=${pairSampleSize}` : ""})`,
1089
+ );
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ if (highTailPairs.length > 0) {
1095
+ flags.push({
1096
+ type: "warning",
1097
+ dimension: "tailRisk",
1098
+ message: `High tail dependence pairs (>${tailThreshold}): ${highTailPairs.join(", ")}`,
1099
+ });
1100
+ } else {
1101
+ flags.push({
1102
+ type: "pass",
1103
+ dimension: "tailRisk",
1104
+ message: `No tail dependence pairs above ${tailThreshold} threshold`,
1105
+ });
1106
+ }
1148
1107
 
1149
- // WFE below threshold (only if WFA ran)
1150
- if (!wfaSkipped && wfeResult !== null) {
1151
- const normNote = useNormalization ? " (1-lot normalized)" : "";
1152
- if (wfeResult < wfeThresh) {
1108
+ // MC profit probability below threshold
1109
+ if (mcStats.probabilityOfProfit < profitThreshold) {
1153
1110
  flags.push({
1154
1111
  type: "warning",
1155
- dimension: "robustness",
1156
- message: `Walk-forward efficiency${normNote} (${formatPercent(wfeResult * 100)}) below ${formatPercent(wfeThresh * 100)} threshold`,
1112
+ dimension: "consistency",
1113
+ message: `Monte Carlo profit probability (${formatPercent(mcStats.probabilityOfProfit * 100)}) below ${formatPercent(profitThreshold * 100)} threshold`,
1157
1114
  });
1158
1115
  } else {
1159
1116
  flags.push({
1160
1117
  type: "pass",
1161
- dimension: "robustness",
1162
- message: `Walk-forward efficiency${normNote} (${formatPercent(wfeResult * 100)}) meets ${formatPercent(wfeThresh * 100)} threshold`,
1118
+ dimension: "consistency",
1119
+ message: `Monte Carlo profit probability (${formatPercent(mcStats.probabilityOfProfit * 100)}) meets ${formatPercent(profitThreshold * 100)} threshold`,
1163
1120
  });
1164
1121
  }
1165
- if (useNormalization) {
1122
+
1123
+ // MC median MDD vs historical MDD multiplier
1124
+ // mcStats.medianMaxDrawdown is a decimal (0.12 = 12%)
1125
+ // stats.maxDrawdown is a percentage (12 = 12%)
1126
+ // Convert stats.maxDrawdown to decimal for comparison
1127
+ const historicalMddDecimal = stats.maxDrawdown / 100;
1128
+ const mcMddMultiplier =
1129
+ historicalMddDecimal > 0 ? mcStats.medianMaxDrawdown / historicalMddDecimal : null;
1130
+ const mcPctMddMultiplier =
1131
+ historicalMddDecimal > 0 ? mcPctStats.medianMaxDrawdown / historicalMddDecimal : null;
1132
+
1133
+ // Detect position sizing inflation: dollar-mode MDD much higher than percentage-mode
1134
+ const sizingInflated =
1135
+ mcMddMultiplier !== null &&
1136
+ mcPctMddMultiplier !== null &&
1137
+ mcMddMultiplier > 2 * mcPctMddMultiplier;
1138
+
1139
+ if (sizingInflated) {
1140
+ // Dollar-mode MDD is inflated by position sizing growth — report both
1141
+ const pctExceeds = mcPctMddMultiplier! > mddMultThresh;
1166
1142
  flags.push({
1167
- type: "info",
1168
- dimension: "robustness",
1169
- message: `WFE trades normalized to 1-lot (detected pct_of_portfolio sizing in strategy profiles) to remove position sizing growth bias`,
1143
+ type: pctExceeds ? "warning" : "info",
1144
+ dimension: "consistency",
1145
+ message: `Monte Carlo MDD: dollar-mode ${formatPercent(mcStats.medianMaxDrawdown * 100)} (${mcMddMultiplier!.toFixed(1)}x historical) is inflated by position sizing growth. Percentage-mode ${formatPercent(mcPctStats.medianMaxDrawdown * 100)} (${mcPctMddMultiplier!.toFixed(1)}x historical) is more representative for % scaling portfolios`,
1146
+ });
1147
+ } else if (mcMddMultiplier !== null && mcMddMultiplier > mddMultThresh) {
1148
+ flags.push({
1149
+ type: "warning",
1150
+ dimension: "consistency",
1151
+ message: `Monte Carlo median MDD (${formatPercent(mcStats.medianMaxDrawdown * 100)}) is ${mcMddMultiplier.toFixed(1)}x historical MDD (${formatPercent(stats.maxDrawdown)}) - exceeds ${mddMultThresh}x threshold`,
1152
+ });
1153
+ } else if (mcMddMultiplier !== null) {
1154
+ flags.push({
1155
+ type: "pass",
1156
+ dimension: "consistency",
1157
+ message: `Monte Carlo median MDD (${formatPercent(mcStats.medianMaxDrawdown * 100)}) is ${mcMddMultiplier.toFixed(1)}x historical MDD - within ${mddMultThresh}x threshold`,
1170
1158
  });
1171
1159
  }
1172
- }
1173
1160
 
1174
- // =====================================================================
1175
- // NEW: Profile-aware dimensions (5 sections)
1176
- // =====================================================================
1177
-
1178
- // Initialize new grades as null
1179
- let regimeCoverageGrade: Grade | null = null;
1180
- let dayCoverageGrade: Grade | null = null;
1181
- let concentrationGrade: Grade | null = null;
1182
- let correlationRiskGrade: Grade | null = null;
1183
- let scalingAlignmentGrade: Grade | null = null;
1184
-
1185
- // Additional data from profile sections
1186
- let profileSectionData: Record<string, unknown> = {};
1187
- let profileKeyNumbers: Record<string, unknown> = {};
1188
-
1189
- const profiledStrategies = profiles.length;
1190
- const unprofiled = strategies.length - profiles.length;
1191
-
1192
- if (profiles.length === 0) {
1193
- flags.push({
1194
- type: "info",
1195
- dimension: "regimeCoverage",
1196
- message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1197
- });
1198
- flags.push({
1199
- type: "info",
1200
- dimension: "dayCoverage",
1201
- message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1202
- });
1203
- flags.push({
1204
- type: "info",
1205
- dimension: "concentrationRisk",
1206
- message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1207
- });
1208
- flags.push({
1209
- type: "info",
1210
- dimension: "correlationRisk",
1211
- message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1212
- });
1213
- flags.push({
1214
- type: "info",
1215
- dimension: "scalingAlignment",
1216
- message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1217
- });
1218
- } else {
1219
- // Section 1: Regime Coverage Matrix
1220
- try {
1221
- const regime = await buildRegimeCoverageSection(profiles, trades, baseDir);
1222
- regimeCoverageGrade = regime.grade;
1223
- flags.push(...regime.flags);
1224
- profileSectionData = { ...profileSectionData, ...regime.data };
1225
- profileKeyNumbers = { ...profileKeyNumbers, ...regime.keyNumbers };
1226
- } catch {
1161
+ // WFE below threshold (only if WFA ran)
1162
+ if (!wfaSkipped && wfeResult !== null) {
1163
+ const normNote = useNormalization ? " (1-lot normalized)" : "";
1164
+ if (wfeResult < wfeThresh) {
1165
+ flags.push({
1166
+ type: "warning",
1167
+ dimension: "robustness",
1168
+ message: `Walk-forward efficiency${normNote} (${formatPercent(wfeResult * 100)}) below ${formatPercent(wfeThresh * 100)} threshold`,
1169
+ });
1170
+ } else {
1171
+ flags.push({
1172
+ type: "pass",
1173
+ dimension: "robustness",
1174
+ message: `Walk-forward efficiency${normNote} (${formatPercent(wfeResult * 100)}) meets ${formatPercent(wfeThresh * 100)} threshold`,
1175
+ });
1176
+ }
1177
+ if (useNormalization) {
1178
+ flags.push({
1179
+ type: "info",
1180
+ dimension: "robustness",
1181
+ message: `WFE trades normalized to 1-lot (detected pct_of_portfolio sizing in strategy profiles) to remove position sizing growth bias`,
1182
+ });
1183
+ }
1184
+ }
1185
+
1186
+ // =====================================================================
1187
+ // NEW: Profile-aware dimensions (5 sections)
1188
+ // =====================================================================
1189
+
1190
+ // Initialize new grades as null
1191
+ let regimeCoverageGrade: Grade | null = null;
1192
+ let dayCoverageGrade: Grade | null = null;
1193
+ let concentrationGrade: Grade | null = null;
1194
+ let correlationRiskGrade: Grade | null = null;
1195
+ let scalingAlignmentGrade: Grade | null = null;
1196
+
1197
+ // Additional data from profile sections
1198
+ let profileSectionData: Record<string, unknown> = {};
1199
+ let profileKeyNumbers: Record<string, unknown> = {};
1200
+
1201
+ const profiledStrategies = profiles.length;
1202
+ const unprofiled = strategies.length - profiles.length;
1203
+
1204
+ if (profiles.length === 0) {
1227
1205
  flags.push({
1228
1206
  type: "info",
1229
1207
  dimension: "regimeCoverage",
1230
- message: "Skipped: error computing regime coverage matrix",
1208
+ message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1231
1209
  });
1232
- }
1233
-
1234
- // Section 2: Day-of-Week Coverage Heatmap
1235
- try {
1236
- const day = buildDayCoverageSection(profiles);
1237
- dayCoverageGrade = day.grade;
1238
- flags.push(...day.flags);
1239
- profileSectionData = { ...profileSectionData, ...day.data };
1240
- profileKeyNumbers = { ...profileKeyNumbers, ...day.keyNumbers };
1241
- } catch {
1242
1210
  flags.push({
1243
1211
  type: "info",
1244
1212
  dimension: "dayCoverage",
1245
- message: "Skipped: error computing day-of-week coverage",
1213
+ message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1246
1214
  });
1247
- }
1248
-
1249
- // Section 3: Allocation Concentration
1250
- try {
1251
- const conc = buildConcentrationSection(profiles);
1252
- concentrationGrade = conc.grade;
1253
- flags.push(...conc.flags);
1254
- profileSectionData = { ...profileSectionData, ...conc.data };
1255
- } catch {
1256
1215
  flags.push({
1257
1216
  type: "info",
1258
1217
  dimension: "concentrationRisk",
1259
- message: "Skipped: error computing allocation concentration",
1218
+ message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1260
1219
  });
1261
- }
1262
-
1263
- // Section 4: Correlation Risk Flags
1264
- try {
1265
- const corrRisk = buildCorrelationRiskSection(profiles);
1266
- correlationRiskGrade = corrRisk.grade;
1267
- flags.push(...corrRisk.flags);
1268
- profileSectionData = { ...profileSectionData, ...corrRisk.data };
1269
- } catch {
1270
1220
  flags.push({
1271
1221
  type: "info",
1272
1222
  dimension: "correlationRisk",
1273
- message: "Skipped: error computing correlation risk flags",
1223
+ message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1274
1224
  });
1275
- }
1276
-
1277
- // Section 5: Backtest-to-Live Scaling Ratios
1278
- try {
1279
- const scaling = await buildScalingSection(profiles, trades, baseDir, blockId);
1280
- scalingAlignmentGrade = scaling.grade;
1281
- flags.push(...scaling.flags);
1282
- profileSectionData = { ...profileSectionData, ...scaling.data };
1283
- } catch {
1284
1225
  flags.push({
1285
1226
  type: "info",
1286
1227
  dimension: "scalingAlignment",
1287
- message: "Skipped: error computing scaling ratios",
1228
+ message: "Skipped: no strategy profiles found. Use profile_strategy to enable.",
1288
1229
  });
1230
+ } else {
1231
+ // Section 1: Regime Coverage Matrix
1232
+ try {
1233
+ const regime = await buildRegimeCoverageSection(profiles, trades, baseDir);
1234
+ regimeCoverageGrade = regime.grade;
1235
+ flags.push(...regime.flags);
1236
+ profileSectionData = { ...profileSectionData, ...regime.data };
1237
+ profileKeyNumbers = { ...profileKeyNumbers, ...regime.keyNumbers };
1238
+ } catch {
1239
+ flags.push({
1240
+ type: "info",
1241
+ dimension: "regimeCoverage",
1242
+ message: "Skipped: error computing regime coverage matrix",
1243
+ });
1244
+ }
1245
+
1246
+ // Section 2: Day-of-Week Coverage Heatmap
1247
+ try {
1248
+ const day = buildDayCoverageSection(profiles);
1249
+ dayCoverageGrade = day.grade;
1250
+ flags.push(...day.flags);
1251
+ profileSectionData = { ...profileSectionData, ...day.data };
1252
+ profileKeyNumbers = { ...profileKeyNumbers, ...day.keyNumbers };
1253
+ } catch {
1254
+ flags.push({
1255
+ type: "info",
1256
+ dimension: "dayCoverage",
1257
+ message: "Skipped: error computing day-of-week coverage",
1258
+ });
1259
+ }
1260
+
1261
+ // Section 3: Allocation Concentration
1262
+ try {
1263
+ const conc = buildConcentrationSection(profiles);
1264
+ concentrationGrade = conc.grade;
1265
+ flags.push(...conc.flags);
1266
+ profileSectionData = { ...profileSectionData, ...conc.data };
1267
+ } catch {
1268
+ flags.push({
1269
+ type: "info",
1270
+ dimension: "concentrationRisk",
1271
+ message: "Skipped: error computing allocation concentration",
1272
+ });
1273
+ }
1274
+
1275
+ // Section 4: Correlation Risk Flags
1276
+ try {
1277
+ const corrRisk = buildCorrelationRiskSection(profiles);
1278
+ correlationRiskGrade = corrRisk.grade;
1279
+ flags.push(...corrRisk.flags);
1280
+ profileSectionData = { ...profileSectionData, ...corrRisk.data };
1281
+ } catch {
1282
+ flags.push({
1283
+ type: "info",
1284
+ dimension: "correlationRisk",
1285
+ message: "Skipped: error computing correlation risk flags",
1286
+ });
1287
+ }
1288
+
1289
+ // Section 5: Backtest-to-Live Scaling Ratios
1290
+ try {
1291
+ const scaling = await buildScalingSection(profiles, trades, baseDir, blockId);
1292
+ scalingAlignmentGrade = scaling.grade;
1293
+ flags.push(...scaling.flags);
1294
+ profileSectionData = { ...profileSectionData, ...scaling.data };
1295
+ } catch {
1296
+ flags.push({
1297
+ type: "info",
1298
+ dimension: "scalingAlignment",
1299
+ message: "Skipped: error computing scaling ratios",
1300
+ });
1301
+ }
1289
1302
  }
1290
- }
1291
1303
 
1292
- // Build grades
1293
- type GradeType = "A" | "B" | "C" | "F";
1294
-
1295
- // Diversification grade based on avg correlation (A: <0.2, B: <0.4, C: <0.6, F: >=0.6)
1296
- let diversificationGrade: GradeType;
1297
- if (avgCorrelation < 0.2) diversificationGrade = "A";
1298
- else if (avgCorrelation < 0.4) diversificationGrade = "B";
1299
- else if (avgCorrelation < 0.6) diversificationGrade = "C";
1300
- else diversificationGrade = "F";
1301
-
1302
- // Tail risk grade based on avg joint tail risk (A: <0.3, B: <0.5, C: <0.7, F: >=0.7)
1303
- let tailRiskGrade: GradeType;
1304
- if (avgTailDependence < 0.3) tailRiskGrade = "A";
1305
- else if (avgTailDependence < 0.5) tailRiskGrade = "B";
1306
- else if (avgTailDependence < 0.7) tailRiskGrade = "C";
1307
- else tailRiskGrade = "F";
1308
-
1309
- // Robustness grade based on WFE (A: >0, B: >-0.1, C: >-0.2, F: <=-0.2), null if WFA skipped
1310
- let robustnessGrade: GradeType | null;
1311
- if (wfaSkipped || wfeResult === null) {
1312
- robustnessGrade = null;
1313
- } else if (wfeResult > 0) {
1314
- robustnessGrade = "A";
1315
- } else if (wfeResult > -0.1) {
1316
- robustnessGrade = "B";
1317
- } else if (wfeResult > -0.2) {
1318
- robustnessGrade = "C";
1319
- } else {
1320
- robustnessGrade = "F";
1321
- }
1304
+ // Build grades
1305
+ type GradeType = "A" | "B" | "C" | "F";
1306
+
1307
+ // Diversification grade based on avg correlation (A: <0.2, B: <0.4, C: <0.6, F: >=0.6)
1308
+ let diversificationGrade: GradeType;
1309
+ if (avgCorrelation < 0.2) diversificationGrade = "A";
1310
+ else if (avgCorrelation < 0.4) diversificationGrade = "B";
1311
+ else if (avgCorrelation < 0.6) diversificationGrade = "C";
1312
+ else diversificationGrade = "F";
1313
+
1314
+ // Tail risk grade based on avg joint tail risk (A: <0.3, B: <0.5, C: <0.7, F: >=0.7)
1315
+ let tailRiskGrade: GradeType;
1316
+ if (avgTailDependence < 0.3) tailRiskGrade = "A";
1317
+ else if (avgTailDependence < 0.5) tailRiskGrade = "B";
1318
+ else if (avgTailDependence < 0.7) tailRiskGrade = "C";
1319
+ else tailRiskGrade = "F";
1320
+
1321
+ // Robustness grade based on WFE (A: >0, B: >-0.1, C: >-0.2, F: <=-0.2), null if WFA skipped
1322
+ let robustnessGrade: GradeType | null;
1323
+ if (wfaSkipped || wfeResult === null) {
1324
+ robustnessGrade = null;
1325
+ } else if (wfeResult > 0) {
1326
+ robustnessGrade = "A";
1327
+ } else if (wfeResult > -0.1) {
1328
+ robustnessGrade = "B";
1329
+ } else if (wfeResult > -0.2) {
1330
+ robustnessGrade = "C";
1331
+ } else {
1332
+ robustnessGrade = "F";
1333
+ }
1322
1334
 
1323
- // Consistency grade based on MC profit probability (A: >=0.98, B: >=0.90, C: >=0.70, F: <0.70)
1324
- let consistencyGrade: GradeType;
1325
- if (mcStats.probabilityOfProfit >= 0.98) consistencyGrade = "A";
1326
- else if (mcStats.probabilityOfProfit >= 0.9) consistencyGrade = "B";
1327
- else if (mcStats.probabilityOfProfit >= 0.7) consistencyGrade = "C";
1328
- else consistencyGrade = "F";
1329
-
1330
- // Build verdict
1331
- const warningFlags = flags.filter((f) => f.type === "warning");
1332
- const flagCount = warningFlags.length;
1333
- let verdict: "HEALTHY" | "MODERATE_CONCERNS" | "ISSUES_DETECTED";
1334
- let oneLineSummary: string;
1335
-
1336
- if (flagCount === 0) {
1337
- verdict = "HEALTHY";
1338
- oneLineSummary =
1339
- "Portfolio shows strong diversification, controlled tail risk, and consistent Monte Carlo outcomes.";
1340
- } else if (flagCount <= 2) {
1341
- verdict = "MODERATE_CONCERNS";
1342
- const concernDimensions = [
1343
- ...new Set(warningFlags.map((f) => f.dimension)),
1344
- ];
1345
- oneLineSummary = `Portfolio has ${flagCount} warning(s) in ${concernDimensions.join(", ")} - review flagged items.`;
1346
- } else {
1347
- verdict = "ISSUES_DETECTED";
1348
- const concernDimensions = [
1349
- ...new Set(warningFlags.map((f) => f.dimension)),
1350
- ];
1351
- oneLineSummary = `Portfolio has ${flagCount} warnings across ${concernDimensions.join(", ")} - significant review recommended.`;
1352
- }
1335
+ // Consistency grade based on MC profit probability (A: >=0.98, B: >=0.90, C: >=0.70, F: <0.70)
1336
+ let consistencyGrade: GradeType;
1337
+ if (mcStats.probabilityOfProfit >= 0.98) consistencyGrade = "A";
1338
+ else if (mcStats.probabilityOfProfit >= 0.9) consistencyGrade = "B";
1339
+ else if (mcStats.probabilityOfProfit >= 0.7) consistencyGrade = "C";
1340
+ else consistencyGrade = "F";
1341
+
1342
+ // Build verdict
1343
+ const warningFlags = flags.filter((f) => f.type === "warning");
1344
+ const flagCount = warningFlags.length;
1345
+ let verdict: "HEALTHY" | "MODERATE_CONCERNS" | "ISSUES_DETECTED";
1346
+ let oneLineSummary: string;
1347
+
1348
+ if (flagCount === 0) {
1349
+ verdict = "HEALTHY";
1350
+ oneLineSummary =
1351
+ "Portfolio shows strong diversification, controlled tail risk, and consistent Monte Carlo outcomes.";
1352
+ } else if (flagCount <= 2) {
1353
+ verdict = "MODERATE_CONCERNS";
1354
+ const concernDimensions = [...new Set(warningFlags.map((f) => f.dimension))];
1355
+ oneLineSummary = `Portfolio has ${flagCount} warning(s) in ${concernDimensions.join(", ")} - review flagged items.`;
1356
+ } else {
1357
+ verdict = "ISSUES_DETECTED";
1358
+ const concernDimensions = [...new Set(warningFlags.map((f) => f.dimension))];
1359
+ oneLineSummary = `Portfolio has ${flagCount} warnings across ${concernDimensions.join(", ")} - significant review recommended.`;
1360
+ }
1361
+
1362
+ // Build key numbers
1363
+ // Note: stats.maxDrawdown is already in percentage form (e.g., 5.66 = 5.66%)
1364
+ const keyNumbers = {
1365
+ strategies: strategies.length,
1366
+ trades: trades.length,
1367
+ sharpe: stats.sharpeRatio,
1368
+ sortino: stats.sortinoRatio,
1369
+ maxDrawdownPct: stats.maxDrawdown, // Already a percentage
1370
+ netPl: stats.netPl,
1371
+ avgCorrelation,
1372
+ avgTailDependence,
1373
+ mcProbabilityOfProfit: mcStats.probabilityOfProfit,
1374
+ mcMedianMdd: mcStats.medianMaxDrawdown,
1375
+ mcMddMultiplier,
1376
+ mcPctMedianMdd: mcPctStats.medianMaxDrawdown,
1377
+ mcPctMddMultiplier,
1378
+ mcSizingInflated: sizingInflated,
1379
+ wfe: wfeResult,
1380
+ wfeNormalized: useNormalization,
1381
+ // NEW profile-aware key numbers
1382
+ profiledStrategies,
1383
+ unprofiled,
1384
+ ...profileKeyNumbers,
1385
+ };
1353
1386
 
1354
- // Build key numbers
1355
- // Note: stats.maxDrawdown is already in percentage form (e.g., 5.66 = 5.66%)
1356
- const keyNumbers = {
1357
- strategies: strategies.length,
1358
- trades: trades.length,
1359
- sharpe: stats.sharpeRatio,
1360
- sortino: stats.sortinoRatio,
1361
- maxDrawdownPct: stats.maxDrawdown, // Already a percentage
1362
- netPl: stats.netPl,
1363
- avgCorrelation,
1364
- avgTailDependence,
1365
- mcProbabilityOfProfit: mcStats.probabilityOfProfit,
1366
- mcMedianMdd: mcStats.medianMaxDrawdown,
1367
- mcMddMultiplier,
1368
- mcPctMedianMdd: mcPctStats.medianMaxDrawdown,
1369
- mcPctMddMultiplier,
1370
- mcSizingInflated: sizingInflated,
1371
- wfe: wfeResult,
1372
- wfeNormalized: useNormalization,
1373
- // NEW profile-aware key numbers
1374
- profiledStrategies,
1375
- unprofiled,
1376
- ...profileKeyNumbers,
1377
- };
1378
-
1379
- // Build grades object
1380
- const grades = {
1381
- diversification: diversificationGrade,
1382
- tailRisk: tailRiskGrade,
1383
- robustness: robustnessGrade,
1384
- consistency: consistencyGrade,
1385
- // NEW profile-aware grades
1386
- regimeCoverage: regimeCoverageGrade,
1387
- dayCoverage: dayCoverageGrade,
1388
- concentrationRisk: concentrationGrade,
1389
- correlationRisk: correlationRiskGrade,
1390
- scalingAlignment: scalingAlignmentGrade,
1391
- };
1392
-
1393
- // Brief summary for user display
1394
- const summary = `Health Check: ${blockId} | ${verdict} | ${flagCount} flags | Sharpe: ${formatRatio(stats.sharpeRatio)} | ${profiledStrategies} profiled`;
1395
-
1396
- // Build structured data
1397
- const structuredData = {
1398
- blockId,
1399
- thresholds: {
1400
- correlationThreshold: corrThreshold,
1401
- tailDependenceThreshold: tailThreshold,
1402
- profitProbabilityThreshold: profitThreshold,
1403
- wfeThreshold: wfeThresh,
1404
- mddMultiplierThreshold: mddMultThresh,
1405
- },
1406
- verdict: {
1407
- status: verdict,
1408
- oneLineSummary,
1409
- flagCount,
1410
- },
1411
- grades,
1412
- flags,
1413
- keyNumbers,
1414
- // NEW profile-aware section data
1415
- ...profileSectionData,
1416
- };
1387
+ // Build grades object
1388
+ const grades = {
1389
+ diversification: diversificationGrade,
1390
+ tailRisk: tailRiskGrade,
1391
+ robustness: robustnessGrade,
1392
+ consistency: consistencyGrade,
1393
+ // NEW profile-aware grades
1394
+ regimeCoverage: regimeCoverageGrade,
1395
+ dayCoverage: dayCoverageGrade,
1396
+ concentrationRisk: concentrationGrade,
1397
+ correlationRisk: correlationRiskGrade,
1398
+ scalingAlignment: scalingAlignmentGrade,
1399
+ };
1400
+
1401
+ // Brief summary for user display
1402
+ const summary = `Health Check: ${blockId} | ${verdict} | ${flagCount} flags | Sharpe: ${formatRatio(stats.sharpeRatio)} | ${profiledStrategies} profiled`;
1403
+
1404
+ // Build structured data
1405
+ const structuredData = {
1406
+ blockId,
1407
+ thresholds: {
1408
+ correlationThreshold: corrThreshold,
1409
+ tailDependenceThreshold: tailThreshold,
1410
+ profitProbabilityThreshold: profitThreshold,
1411
+ wfeThreshold: wfeThresh,
1412
+ mddMultiplierThreshold: mddMultThresh,
1413
+ },
1414
+ verdict: {
1415
+ status: verdict,
1416
+ oneLineSummary,
1417
+ flagCount,
1418
+ },
1419
+ grades,
1420
+ flags,
1421
+ keyNumbers,
1422
+ // NEW profile-aware section data
1423
+ ...profileSectionData,
1424
+ };
1417
1425
 
1418
1426
  return createToolOutput(summary, structuredData);
1419
1427
  } catch (error) {
@@ -1427,7 +1435,7 @@ export function registerHealthBlockTools(
1427
1435
  isError: true as const,
1428
1436
  };
1429
1437
  }
1430
- }
1431
- )
1438
+ },
1439
+ ),
1432
1440
  );
1433
1441
  }