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