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.
- package/README.md +82 -68
- package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
- package/dist/chunk-27S67XW3.js.map +1 -0
- package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
- package/dist/chunk-2E63THNI.js.map +1 -0
- package/dist/{chunk-BKQ4PM4Y.js → chunk-6S37CXUA.js} +11 -13
- package/dist/chunk-6S37CXUA.js.map +1 -0
- package/dist/{chunk-PRAYH3RT.js → chunk-AP7IHUUR.js} +29 -12
- package/dist/chunk-AP7IHUUR.js.map +1 -0
- package/dist/{chunk-QTTR7AAW.js → chunk-C6LL746C.js} +67 -84
- package/dist/{chunk-QTTR7AAW.js.map → chunk-C6LL746C.js.map} +1 -1
- package/dist/{chunk-W2PP3LEH.js → chunk-SEUZYQGQ.js} +17 -8
- package/dist/chunk-SEUZYQGQ.js.map +1 -0
- package/dist/{chunk-4BLCXNQ6.js → chunk-UBUC5A66.js} +105 -114
- package/dist/chunk-UBUC5A66.js.map +1 -0
- package/dist/{chunk-XXYOUIZY.js → chunk-VDU25Z6X.js} +63 -80
- package/dist/chunk-VDU25Z6X.js.map +1 -0
- package/dist/daily-log-processor-BY3ISY6K.js +9 -0
- package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
- package/dist/iv-solver-worker.js +9 -1
- package/dist/market-provider-H3ARIVZ4.js +16 -0
- package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
- package/dist/{sync-PO4IPCYV.js → sync-EVLKB3ZJ.js} +7 -7
- package/dist/test-exports.js +358 -254
- package/dist/test-exports.js.map +1 -1
- package/dist/trade-processor-EYA3I3XB.js +9 -0
- package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
- package/manifest.json +3 -11
- package/package.json +1 -1
- package/server/{chunk-NZO6PT64.js → chunk-4P7D7YZP.js} +102 -113
- package/server/chunk-4P7D7YZP.js.map +1 -0
- package/server/{chunk-72GKJE2U.js → chunk-BOPHW5M6.js} +521 -235
- package/server/chunk-BOPHW5M6.js.map +1 -0
- package/server/{chunk-WA5AAPCH.js → chunk-GH2552SE.js} +17 -8
- package/server/chunk-GH2552SE.js.map +1 -0
- package/server/{chunk-5EBXHT6C.js → chunk-OBYKFW2B.js} +11 -13
- package/server/chunk-OBYKFW2B.js.map +1 -0
- package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
- package/server/chunk-T66KH2XH.js.map +1 -0
- package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
- package/server/chunk-W5E7FHC4.js.map +1 -0
- package/server/{chunk-CSDVJPBB.js → chunk-YUCOAJ4Z.js} +29 -12
- package/server/chunk-YUCOAJ4Z.js.map +1 -0
- package/server/{chunk-PNKG7RY7.js → chunk-ZBJCF4ZG.js} +63 -80
- package/server/chunk-ZBJCF4ZG.js.map +1 -0
- package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
- package/server/config-DK7KOMNL.js.map +1 -0
- package/server/daily-log-processor-ENEUT22A.js +10 -0
- package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
- package/server/http-server.js +2 -5
- package/server/index.js +1771 -1538
- package/server/index.js.map +1 -1
- package/server/iv-solver-worker.js +9 -1
- package/server/market-provider-B437HKLW.js +17 -0
- package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
- package/server/{sync-QFI5L7S7.js → sync-V25UQJA3.js} +7 -7
- package/server/trade-processor-L3PIQ5TG.js +10 -0
- package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
- package/src/auth/clients-store.ts +4 -4
- package/src/auth/code-store.ts +1 -1
- package/src/auth/config.ts +11 -12
- package/src/auth/login-page.ts +8 -10
- package/src/auth/provider.ts +35 -30
- package/src/auth/token.ts +17 -15
- package/src/db/backtest-schemas.ts +12 -4
- package/src/db/connection.ts +107 -40
- package/src/db/index.ts +37 -5
- package/src/db/json-adapters.ts +22 -31
- package/src/db/json-migration.ts +11 -15
- package/src/db/json-store.ts +5 -3
- package/src/db/market-datasets.ts +48 -15
- package/src/db/market-schemas.ts +11 -5
- package/src/db/market-views.ts +119 -32
- package/src/db/parquet-writer.ts +7 -7
- package/src/db/profile-schemas.ts +39 -22
- package/src/db/schemas.ts +2 -2
- package/src/http-server.ts +7 -12
- package/src/index.ts +8 -9
- package/src/market/ingestor/index.ts +1 -4
- package/src/market/ingestor/market-ingestor.ts +140 -67
- package/src/market/ingestor/types.ts +1 -3
- package/src/market/stores/chain-sql.ts +2 -8
- package/src/market/stores/chain-store.ts +1 -4
- package/src/market/stores/duckdb-chain-store.ts +3 -14
- package/src/market/stores/duckdb-enriched-store.ts +3 -13
- package/src/market/stores/duckdb-quote-store.ts +10 -27
- package/src/market/stores/duckdb-spot-store.ts +4 -20
- package/src/market/stores/enriched-sql.ts +3 -9
- package/src/market/stores/enriched-store.ts +2 -2
- package/src/market/stores/parquet-chain-store.ts +5 -22
- package/src/market/stores/parquet-enriched-store.ts +4 -16
- package/src/market/stores/parquet-oi-daily-store.ts +3 -14
- package/src/market/stores/parquet-quote-store.ts +9 -29
- package/src/market/stores/parquet-spot-store.ts +14 -33
- package/src/market/stores/quote-store.ts +2 -10
- package/src/market/stores/spot-sql.ts +3 -15
- package/src/market/stores/spot-store.ts +1 -1
- package/src/market/tickers/defaults.json +6 -6
- package/src/market/tickers/loader.ts +2 -7
- package/src/market/tickers/registry.ts +5 -15
- package/src/market/tickers/schemas.ts +2 -6
- package/src/models/strategy-profile.ts +39 -39
- package/src/sync/block-sync.ts +44 -65
- package/src/sync/index.ts +1 -4
- package/src/sync/metadata.ts +14 -20
- package/src/test-exports.ts +127 -123
- package/src/tools/analysis.ts +180 -159
- package/src/tools/batch-exit-analysis.ts +100 -59
- package/src/tools/blocks/analysis.ts +116 -101
- package/src/tools/blocks/comparison.ts +281 -364
- package/src/tools/blocks/core.ts +208 -253
- package/src/tools/blocks/health.ts +571 -563
- package/src/tools/blocks/similarity.ts +316 -283
- package/src/tools/edge-decay.ts +124 -197
- package/src/tools/exit-analysis.ts +130 -77
- package/src/tools/greeks-attribution.ts +84 -35
- package/src/tools/guides.ts +4 -6
- package/src/tools/imports.ts +9 -12
- package/src/tools/market-data.ts +1067 -757
- package/src/tools/market-enrichment.ts +3 -3
- package/src/tools/market-fetch.ts +148 -67
- package/src/tools/market-imports.ts +12 -12
- package/src/tools/middleware/sync-middleware.ts +5 -6
- package/src/tools/performance.ts +185 -302
- package/src/tools/profile-analysis.ts +52 -66
- package/src/tools/profiles.ts +106 -69
- package/src/tools/regime-advisor.ts +20 -45
- package/src/tools/replay.ts +81 -77
- package/src/tools/reports/discrepancies.ts +298 -328
- package/src/tools/reports/fields.ts +7 -25
- package/src/tools/reports/helpers.ts +18 -49
- package/src/tools/reports/predictive.ts +27 -70
- package/src/tools/reports/slippage-trends.ts +315 -345
- package/src/tools/reports/slippage.ts +1 -4
- package/src/tools/reports/strategy-matches.ts +399 -441
- package/src/tools/schema.ts +43 -40
- package/src/tools/shared/filters.ts +3 -9
- package/src/tools/snapshot.ts +9 -30
- package/src/tools/sql.ts +15 -14
- package/src/tools/tickers.ts +1 -4
- package/src/utils/batch-exit-analysis.ts +31 -42
- package/src/utils/black-scholes.ts +39 -29
- package/src/utils/block-loader.ts +68 -82
- package/src/utils/calibration-probe.ts +1 -2
- package/src/utils/chain-loader.ts +3 -3
- package/src/utils/csv-discovery.ts +16 -22
- package/src/utils/data-quality.ts +24 -36
- package/src/utils/exit-triggers.ts +91 -96
- package/src/utils/field-timing.ts +94 -79
- package/src/utils/filter-predicates.ts +13 -9
- package/src/utils/flatfile-importer.ts +94 -64
- package/src/utils/greeks-decomposition.ts +152 -100
- package/src/utils/iv-solver-pool.ts +55 -25
- package/src/utils/iv-solver-worker.ts +5 -5
- package/src/utils/market-enricher.ts +528 -497
- package/src/utils/market-importer.ts +31 -12
- package/src/utils/market-provider.ts +21 -23
- package/src/utils/massive-tier.ts +5 -7
- package/src/utils/migrate-option-data-helpers.ts +2 -8
- package/src/utils/option-quote-greeks.ts +25 -31
- package/src/utils/option-time.ts +4 -8
- package/src/utils/output-formatter.ts +1 -4
- package/src/utils/provider-capabilities.ts +1 -4
- package/src/utils/providers/massive.ts +59 -93
- package/src/utils/providers/thetadata/backfill.ts +14 -23
- package/src/utils/providers/thetadata/client.ts +12 -8
- package/src/utils/providers/thetadata/decode.ts +2 -20
- package/src/utils/providers/thetadata/endpoints.ts +32 -40
- package/src/utils/providers/thetadata/join.ts +11 -10
- package/src/utils/providers/thetadata/proto.ts +12 -10
- package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
- package/src/utils/providers/thetadata.ts +11 -10
- package/src/utils/quote-enricher.ts +4 -4
- package/src/utils/quote-parquet-projection.ts +3 -11
- package/src/utils/sample-date-selector.ts +3 -5
- package/src/utils/schema-metadata.ts +102 -70
- package/src/utils/ticker.ts +5 -9
- package/src/utils/trade-replay.ts +77 -68
- package/dist/chunk-4BLCXNQ6.js.map +0 -1
- package/dist/chunk-BKQ4PM4Y.js.map +0 -1
- package/dist/chunk-FGZH632F.js.map +0 -1
- package/dist/chunk-LDKTV7GW.js.map +0 -1
- package/dist/chunk-PRAYH3RT.js.map +0 -1
- package/dist/chunk-W2PP3LEH.js.map +0 -1
- package/dist/chunk-XXYOUIZY.js.map +0 -1
- package/dist/daily-log-processor-6MWJ23JK.js +0 -9
- package/dist/market-provider-VDRJUEF2.js +0 -16
- package/dist/trade-processor-NHU4VWRX.js +0 -9
- package/server/chunk-5EBXHT6C.js.map +0 -1
- package/server/chunk-72GKJE2U.js.map +0 -1
- package/server/chunk-CSDVJPBB.js.map +0 -1
- package/server/chunk-FBNDMCT5.js.map +0 -1
- package/server/chunk-NRFXAJF7.js.map +0 -1
- package/server/chunk-NZO6PT64.js.map +0 -1
- package/server/chunk-PNKG7RY7.js.map +0 -1
- package/server/chunk-WA5AAPCH.js.map +0 -1
- package/server/config-6IZXEFEX.js.map +0 -1
- package/server/daily-log-processor-Y3PVSVBM.js +0 -10
- package/server/market-provider-VOYYVYWT.js +0 -17
- package/server/trade-processor-JWVS37KM.js +0 -10
- /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-BY3ISY6K.js.map} +0 -0
- /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
- /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-H3ARIVZ4.js.map} +0 -0
- /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
- /package/dist/{sync-PO4IPCYV.js.map → sync-EVLKB3ZJ.js.map} +0 -0
- /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-EYA3I3XB.js.map} +0 -0
- /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
- /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-ENEUT22A.js.map} +0 -0
- /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
- /package/server/{market-provider-VOYYVYWT.js.map → market-provider-B437HKLW.js.map} +0 -0
- /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
- /package/server/{sync-QFI5L7S7.js.map → sync-V25UQJA3.js.map} +0 -0
- /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-L3PIQ5TG.js.map} +0 -0
- /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,
|
|
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<
|
|
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: {
|
|
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 ??
|
|
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 =
|
|
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({
|
|
467
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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:
|
|
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<
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
952
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
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: "
|
|
1156
|
-
message: `
|
|
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: "
|
|
1162
|
-
message: `
|
|
1118
|
+
dimension: "consistency",
|
|
1119
|
+
message: `Monte Carlo profit probability (${formatPercent(mcStats.probabilityOfProfit * 100)}) meets ${formatPercent(profitThreshold * 100)} threshold`,
|
|
1163
1120
|
});
|
|
1164
1121
|
}
|
|
1165
|
-
|
|
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: "
|
|
1169
|
-
message: `
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
}
|