tradeblocks-mcp 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +40 -1
  2. package/dist/{chunk-YRD62LDE.js → chunk-35RVWZGT.js} +4 -1
  3. package/dist/{chunk-4UU7IT2A.js.map → chunk-35RVWZGT.js.map} +1 -1
  4. package/dist/{chunk-4UU7IT2A.js → chunk-ENXBPJ7Y.js} +148 -6
  5. package/dist/chunk-ENXBPJ7Y.js.map +1 -0
  6. package/dist/{chunk-62GCLMQA.js → chunk-TLX7IBCM.js} +3 -6
  7. package/dist/chunk-TLX7IBCM.js.map +1 -0
  8. package/dist/{daily-logs-store-PXB53WEK.js → daily-logs-store-FZRIGYUX.js} +2 -2
  9. package/dist/{daily-logs-store-65YHFJ5F.js → daily-logs-store-MEZ5S3BR.js} +2 -2
  10. package/dist/market/provenance/index.js +1 -4
  11. package/dist/market/provenance/index.js.map +1 -1
  12. package/dist/{sync-JL7OPLCT.js → sync-EW33QZPD.js} +3 -3
  13. package/dist/test-exports.js +320 -279
  14. package/dist/test-exports.js.map +1 -1
  15. package/dist/{trades-store-UGZ62A7N.js → trades-store-3TOLBL2W.js} +2 -2
  16. package/dist/{trades-store-MOFJHRC2.js → trades-store-PNLPRYRJ.js} +2 -2
  17. package/package.json +1 -1
  18. package/server/{chunk-7N6HBOAY.js → chunk-PDSUY2X2.js} +148 -6
  19. package/server/chunk-PDSUY2X2.js.map +1 -0
  20. package/server/{chunk-J4GWE6JR.js → chunk-YIWLVX7Z.js} +3 -6
  21. package/server/chunk-YIWLVX7Z.js.map +1 -0
  22. package/server/{daily-logs-store-YAXASQN4.js → daily-logs-store-LRVO7COL.js} +2 -2
  23. package/server/index.js +993 -801
  24. package/server/index.js.map +1 -1
  25. package/server/{sync-ZV3QEGMO.js → sync-2POG6KA6.js} +3 -3
  26. package/server/{trades-store-MF7YYLCH.js → trades-store-MXUZLSF5.js} +2 -2
  27. package/src/db/dataset-schemas.ts +97 -23
  28. package/src/tools/market-data.ts +327 -245
  29. package/src/tools/profile-analysis.ts +18 -4
  30. package/src/tools/reports/fields.ts +16 -11
  31. package/src/tools/reports/helpers.ts +6 -2
  32. package/src/tools/reports/predictive.ts +15 -4
  33. package/src/tools/shared/field-analysis.ts +57 -14
  34. package/src/tools/shared/filter-objective.ts +73 -0
  35. package/src/utils/trade-analysis-fields.ts +19 -4
  36. package/dist/chunk-62GCLMQA.js.map +0 -1
  37. package/dist/chunk-YRD62LDE.js.map +0 -1
  38. package/server/chunk-7N6HBOAY.js.map +0 -1
  39. package/server/chunk-J4GWE6JR.js.map +0 -1
  40. /package/dist/{daily-logs-store-65YHFJ5F.js.map → daily-logs-store-FZRIGYUX.js.map} +0 -0
  41. /package/dist/{daily-logs-store-PXB53WEK.js.map → daily-logs-store-MEZ5S3BR.js.map} +0 -0
  42. /package/dist/{sync-JL7OPLCT.js.map → sync-EW33QZPD.js.map} +0 -0
  43. /package/dist/{trades-store-MOFJHRC2.js.map → trades-store-3TOLBL2W.js.map} +0 -0
  44. /package/dist/{trades-store-UGZ62A7N.js.map → trades-store-PNLPRYRJ.js.map} +0 -0
  45. /package/server/{daily-logs-store-YAXASQN4.js.map → daily-logs-store-LRVO7COL.js.map} +0 -0
  46. /package/server/{sync-ZV3QEGMO.js.map → sync-2POG6KA6.js.map} +0 -0
  47. /package/server/{trades-store-MF7YYLCH.js.map → trades-store-MXUZLSF5.js.map} +0 -0
@@ -37,8 +37,20 @@ import {
37
37
  import { checkDataAvailability } from "../utils/data-availability.ts";
38
38
  import { getProfile } from "../db/profile-schemas.ts";
39
39
  import type { MarketStores } from "../market/stores/index.ts";
40
- import { listDatasets, resolveTradeDatasets } from "../db/dataset-schemas.ts";
40
+ import {
41
+ listDatasets,
42
+ resolveTradeDatasets,
43
+ maxMissedSessionsSchema,
44
+ summarizeDatasetAge,
45
+ datasetAgeOutput,
46
+ } from "../db/dataset-schemas.ts";
41
47
  import { suggestFieldFilters, analyzeFieldRegimes } from "./shared/field-analysis.ts";
48
+ import {
49
+ suggestionEvidence,
50
+ rankSuggestions,
51
+ objectiveOutput,
52
+ type SuggestionEvidence,
53
+ } from "./shared/filter-objective.ts";
42
54
 
43
55
  // =============================================================================
44
56
  // Types
@@ -279,6 +291,7 @@ export function registerMarketDataTools(
279
291
  "Returns warnings when market data is partially missing.",
280
292
  inputSchema: z.object({
281
293
  blockId: z.string().describe("Block ID to analyze"),
294
+ maxMissedSessions: maxMissedSessionsSchema,
282
295
  field: z
283
296
  .string()
284
297
  .optional()
@@ -300,229 +313,240 @@ export function registerMarketDataTools(
300
313
  ticker: z.string().optional().describe("Underlying ticker symbol (default: SPX)"),
301
314
  }),
302
315
  },
303
- withFullSync(baseDir, async ({ blockId, segmentBy, strategy, ticker, field, bins }) => {
304
- try {
305
- const block = await loadBlock(baseDir, blockId);
306
- let trades = block.trades;
316
+ withFullSync(
317
+ baseDir,
318
+ async ({ blockId, segmentBy, strategy, ticker, field, bins, maxMissedSessions }) => {
319
+ try {
320
+ const block = await loadBlock(baseDir, blockId);
321
+ let trades = block.trades;
307
322
 
308
- if (strategy) {
309
- trades = trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
310
- }
323
+ if (strategy) {
324
+ trades = trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
325
+ }
311
326
 
312
- if (trades.length === 0) {
313
- return {
314
- content: [{ type: "text", text: "No trades found" }],
315
- isError: true,
316
- };
317
- }
327
+ if (trades.length === 0) {
328
+ return {
329
+ content: [{ type: "text", text: "No trades found" }],
330
+ isError: true,
331
+ };
332
+ }
318
333
 
319
- if (field) return await analyzeFieldRegimes(baseDir, blockId, trades, field, bins);
320
- if (!segmentBy) throw new Error("Provide field or segmentBy");
334
+ if (field)
335
+ return await analyzeFieldRegimes(
336
+ baseDir,
337
+ blockId,
338
+ trades,
339
+ field,
340
+ bins,
341
+ maxMissedSessions,
342
+ );
343
+ if (!segmentBy) throw new Error("Provide field or segmentBy");
321
344
 
322
- // Collect unique trade keys (ticker+date) and query DuckDB for market data
323
- const tradeKeys = uniqueTradeLookupKeys(trades);
345
+ // Collect unique trade keys (ticker+date) and query DuckDB for market data
346
+ const tradeKeys = uniqueTradeLookupKeys(trades);
324
347
 
325
- const conn = await getConnection(baseDir);
348
+ const conn = await getConnection(baseDir);
326
349
 
327
- // Check data availability and collect warnings
328
- const resolvedTicker = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
329
- const availability = await checkDataAvailability(stores, resolvedTicker);
350
+ // Check data availability and collect warnings
351
+ const resolvedTicker = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
352
+ const availability = await checkDataAvailability(stores, resolvedTicker);
330
353
 
331
- const { sql: lagSql, params: lagParams } = buildLookaheadFreeQuery(tradeKeys);
332
- const dailyResult = await conn.runAndReadAll(lagSql, lagParams);
333
- const dailyRecords = resultToRecords(dailyResult);
334
- const daily = recordsByTickerDate(dailyRecords);
354
+ const { sql: lagSql, params: lagParams } = buildLookaheadFreeQuery(tradeKeys);
355
+ const dailyResult = await conn.runAndReadAll(lagSql, lagParams);
356
+ const dailyRecords = resultToRecords(dailyResult);
357
+ const daily = recordsByTickerDate(dailyRecords);
335
358
 
336
- // Match trades to market data and segment
337
- interface SegmentStats {
338
- segment: string;
339
- segmentValue: number | string;
340
- trades: { pl: number; isWin: boolean }[];
341
- }
359
+ // Match trades to market data and segment
360
+ interface SegmentStats {
361
+ segment: string;
362
+ segmentValue: number | string;
363
+ trades: { pl: number; isWin: boolean }[];
364
+ }
342
365
 
343
- const segments = new Map<string, SegmentStats>();
344
- let totalMatched = 0;
345
- let totalWins = 0;
346
- let totalPl = 0;
347
- let lagExcluded = 0;
348
- const unmatchedDates: string[] = [];
366
+ const segments = new Map<string, SegmentStats>();
367
+ let totalMatched = 0;
368
+ let totalWins = 0;
369
+ let totalPl = 0;
370
+ let lagExcluded = 0;
371
+ const unmatchedDates: string[] = [];
349
372
 
350
- for (const trade of trades) {
351
- const lookup = getTradeLookupKey(trade);
352
- const marketData = daily.get(marketTickerDateKey(lookup.ticker, lookup.date));
373
+ for (const trade of trades) {
374
+ const lookup = getTradeLookupKey(trade);
375
+ const marketData = daily.get(marketTickerDateKey(lookup.ticker, lookup.date));
353
376
 
354
- if (!marketData) {
355
- unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
356
- continue;
357
- }
377
+ if (!marketData) {
378
+ unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
379
+ continue;
380
+ }
358
381
 
359
- // Get segment value (must resolve before counting in totals,
360
- // since lagged fields may be NaN and cause continue)
361
- const isWin = trade.pl > 0;
362
- let segmentKey: string;
363
- let segmentLabel: string;
364
- let segmentValue: number | string;
365
-
366
- switch (segmentBy) {
367
- case "volRegime": {
368
- const val = getNum(marketData, "prev_Vol_Regime");
369
- if (isNaN(val)) {
370
- lagExcluded++;
371
- continue;
382
+ // Get segment value (must resolve before counting in totals,
383
+ // since lagged fields may be NaN and cause continue)
384
+ const isWin = trade.pl > 0;
385
+ let segmentKey: string;
386
+ let segmentLabel: string;
387
+ let segmentValue: number | string;
388
+
389
+ switch (segmentBy) {
390
+ case "volRegime": {
391
+ const val = getNum(marketData, "prev_Vol_Regime");
392
+ if (isNaN(val)) {
393
+ lagExcluded++;
394
+ continue;
395
+ }
396
+ segmentValue = val;
397
+ segmentKey = String(val);
398
+ segmentLabel = getVolRegimeLabel(val);
399
+ break;
372
400
  }
373
- segmentValue = val;
374
- segmentKey = String(val);
375
- segmentLabel = getVolRegimeLabel(val);
376
- break;
377
- }
378
- case "termStructure": {
379
- const val = getNum(marketData, "prev_Term_Structure_State");
380
- if (isNaN(val)) {
381
- lagExcluded++;
382
- continue;
401
+ case "termStructure": {
402
+ const val = getNum(marketData, "prev_Term_Structure_State");
403
+ if (isNaN(val)) {
404
+ lagExcluded++;
405
+ continue;
406
+ }
407
+ segmentValue = val;
408
+ segmentKey = String(val);
409
+ segmentLabel = getTermStructureLabel(val);
410
+ break;
383
411
  }
384
- segmentValue = val;
385
- segmentKey = String(val);
386
- segmentLabel = getTermStructureLabel(val);
387
- break;
388
- }
389
- case "dayOfWeek":
390
- segmentValue = getNum(marketData, "Day_of_Week");
391
- segmentKey = String(segmentValue);
392
- segmentLabel = getDayLabel(getNum(marketData, "Day_of_Week"));
393
- break;
394
- case "gapDirection": {
395
- const gapPct = getNum(marketData, "Gap_Pct");
396
- if (isNaN(gapPct)) {
397
- lagExcluded++;
398
- continue;
412
+ case "dayOfWeek":
413
+ segmentValue = getNum(marketData, "Day_of_Week");
414
+ segmentKey = String(segmentValue);
415
+ segmentLabel = getDayLabel(getNum(marketData, "Day_of_Week"));
416
+ break;
417
+ case "gapDirection": {
418
+ const gapPct = getNum(marketData, "Gap_Pct");
419
+ if (isNaN(gapPct)) {
420
+ lagExcluded++;
421
+ continue;
422
+ }
423
+ segmentValue = gapPct > 0.1 ? "up" : gapPct < -0.1 ? "down" : "flat";
424
+ segmentKey = segmentValue;
425
+ segmentLabel = `Gap ${segmentValue}`;
426
+ break;
399
427
  }
400
- segmentValue = gapPct > 0.1 ? "up" : gapPct < -0.1 ? "down" : "flat";
401
- segmentKey = segmentValue;
402
- segmentLabel = `Gap ${segmentValue}`;
403
- break;
428
+ default:
429
+ continue;
404
430
  }
405
- default:
406
- continue;
431
+
432
+ // Count in totals only after segment resolved (NaN-lag trades excluded)
433
+ totalMatched++;
434
+ if (isWin) totalWins++;
435
+ totalPl += trade.pl;
436
+
437
+ if (!segments.has(segmentKey)) {
438
+ segments.set(segmentKey, {
439
+ segment: segmentLabel,
440
+ segmentValue,
441
+ trades: [],
442
+ });
443
+ }
444
+
445
+ segments.get(segmentKey)!.trades.push({ pl: trade.pl, isWin });
407
446
  }
408
447
 
409
- // Count in totals only after segment resolved (NaN-lag trades excluded)
410
- totalMatched++;
411
- if (isWin) totalWins++;
412
- totalPl += trade.pl;
448
+ if (totalMatched === 0) {
449
+ return {
450
+ content: [{ type: "text", text: "No trades matched to market data" }],
451
+ isError: true,
452
+ };
453
+ }
413
454
 
414
- if (!segments.has(segmentKey)) {
415
- segments.set(segmentKey, {
416
- segment: segmentLabel,
417
- segmentValue,
418
- trades: [],
455
+ // Calculate overall stats
456
+ const overallWinRate = (totalWins / totalMatched) * 100;
457
+ const overallAvgPl = totalPl / totalMatched;
458
+
459
+ // Calculate segment stats
460
+ const segmentStats = Array.from(segments.values())
461
+ .map((seg) => {
462
+ const wins = seg.trades.filter((t) => t.isWin).length;
463
+ const losses = seg.trades.length - wins;
464
+ const winRate = (wins / seg.trades.length) * 100;
465
+ const totalSegPl = seg.trades.reduce((sum, t) => sum + t.pl, 0);
466
+ const avgPl = totalSegPl / seg.trades.length;
467
+
468
+ const winningTrades = seg.trades.filter((t) => t.isWin);
469
+ const losingTrades = seg.trades.filter((t) => !t.isWin);
470
+ const avgWin =
471
+ winningTrades.length > 0
472
+ ? winningTrades.reduce((sum, t) => sum + t.pl, 0) / winningTrades.length
473
+ : 0;
474
+ const avgLoss =
475
+ losingTrades.length > 0
476
+ ? losingTrades.reduce((sum, t) => sum + t.pl, 0) / losingTrades.length
477
+ : 0;
478
+
479
+ const grossWins = winningTrades.reduce((sum, t) => sum + t.pl, 0);
480
+ const grossLosses = Math.abs(losingTrades.reduce((sum, t) => sum + t.pl, 0));
481
+ const profitFactor =
482
+ grossLosses > 0 ? grossWins / grossLosses : grossWins > 0 ? null : 0;
483
+
484
+ return {
485
+ segment: seg.segment,
486
+ segmentValue: seg.segmentValue,
487
+ tradeCount: seg.trades.length,
488
+ wins,
489
+ losses,
490
+ winRate: Math.round(winRate * 100) / 100,
491
+ totalPl: Math.round(totalSegPl * 100) / 100,
492
+ avgPl: Math.round(avgPl * 100) / 100,
493
+ avgWin: Math.round(avgWin * 100) / 100,
494
+ avgLoss: Math.round(avgLoss * 100) / 100,
495
+ profitFactor: profitFactor !== null ? Math.round(profitFactor * 100) / 100 : null,
496
+ vsOverallWinRate: Math.round((winRate - overallWinRate) * 100) / 100,
497
+ vsOverallAvgPl: Math.round((avgPl - overallAvgPl) * 100) / 100,
498
+ };
499
+ })
500
+ .sort((a, b) => {
501
+ // Sort by segment value for consistent ordering
502
+ if (typeof a.segmentValue === "number" && typeof b.segmentValue === "number") {
503
+ return a.segmentValue - b.segmentValue;
504
+ }
505
+ return String(a.segmentValue).localeCompare(String(b.segmentValue));
419
506
  });
420
- }
421
507
 
422
- segments.get(segmentKey)!.trades.push({ pl: trade.pl, isWin });
423
- }
508
+ const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
424
509
 
425
- if (totalMatched === 0) {
426
- return {
427
- content: [{ type: "text", text: "No trades matched to market data" }],
428
- isError: true,
429
- };
430
- }
510
+ const summary = `Regime analysis: ${blockId} by ${segmentBy} | ${totalMatched} trades across ${segmentStats.length} segments`;
431
511
 
432
- // Calculate overall stats
433
- const overallWinRate = (totalWins / totalMatched) * 100;
434
- const overallAvgPl = totalPl / totalMatched;
435
-
436
- // Calculate segment stats
437
- const segmentStats = Array.from(segments.values())
438
- .map((seg) => {
439
- const wins = seg.trades.filter((t) => t.isWin).length;
440
- const losses = seg.trades.length - wins;
441
- const winRate = (wins / seg.trades.length) * 100;
442
- const totalSegPl = seg.trades.reduce((sum, t) => sum + t.pl, 0);
443
- const avgPl = totalSegPl / seg.trades.length;
444
-
445
- const winningTrades = seg.trades.filter((t) => t.isWin);
446
- const losingTrades = seg.trades.filter((t) => !t.isWin);
447
- const avgWin =
448
- winningTrades.length > 0
449
- ? winningTrades.reduce((sum, t) => sum + t.pl, 0) / winningTrades.length
450
- : 0;
451
- const avgLoss =
452
- losingTrades.length > 0
453
- ? losingTrades.reduce((sum, t) => sum + t.pl, 0) / losingTrades.length
454
- : 0;
455
-
456
- const grossWins = winningTrades.reduce((sum, t) => sum + t.pl, 0);
457
- const grossLosses = Math.abs(losingTrades.reduce((sum, t) => sum + t.pl, 0));
458
- const profitFactor =
459
- grossLosses > 0 ? grossWins / grossLosses : grossWins > 0 ? null : 0;
512
+ const laggedSegments = ["volRegime", "termStructure"];
513
+ const lagNote = laggedSegments.includes(segmentBy)
514
+ ? `Segmentation by ${segmentBy} uses prior trading day values (close-derived field from market.enriched_context) to prevent lookahead bias.`
515
+ : `Segmentation by ${segmentBy} uses same-day values (${segmentBy === "dayOfWeek" ? "static" : "open-known"} field).`;
460
516
 
461
- return {
462
- segment: seg.segment,
463
- segmentValue: seg.segmentValue,
464
- tradeCount: seg.trades.length,
465
- wins,
466
- losses,
467
- winRate: Math.round(winRate * 100) / 100,
468
- totalPl: Math.round(totalSegPl * 100) / 100,
469
- avgPl: Math.round(avgPl * 100) / 100,
470
- avgWin: Math.round(avgWin * 100) / 100,
471
- avgLoss: Math.round(avgLoss * 100) / 100,
472
- profitFactor: profitFactor !== null ? Math.round(profitFactor * 100) / 100 : null,
473
- vsOverallWinRate: Math.round((winRate - overallWinRate) * 100) / 100,
474
- vsOverallAvgPl: Math.round((avgPl - overallAvgPl) * 100) / 100,
475
- };
476
- })
477
- .sort((a, b) => {
478
- // Sort by segment value for consistent ordering
479
- if (typeof a.segmentValue === "number" && typeof b.segmentValue === "number") {
480
- return a.segmentValue - b.segmentValue;
481
- }
482
- return String(a.segmentValue).localeCompare(String(b.segmentValue));
483
- });
517
+ // Future: Realized_Vol quartile and IVR/IVP regime segmentation dimensions can be added here
484
518
 
485
- const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
486
-
487
- const summary = `Regime analysis: ${blockId} by ${segmentBy} | ${totalMatched} trades across ${segmentStats.length} segments`;
488
-
489
- const laggedSegments = ["volRegime", "termStructure"];
490
- const lagNote = laggedSegments.includes(segmentBy)
491
- ? `Segmentation by ${segmentBy} uses prior trading day values (close-derived field from market.enriched_context) to prevent lookahead bias.`
492
- : `Segmentation by ${segmentBy} uses same-day values (${segmentBy === "dayOfWeek" ? "static" : "open-known"} field).`;
493
-
494
- // Future: Realized_Vol quartile and IVR/IVP regime segmentation dimensions can be added here
495
-
496
- const responseData: Record<string, unknown> = {
497
- blockId,
498
- segmentBy,
499
- lagNote,
500
- strategy: strategy || null,
501
- tradesTotal: trades.length,
502
- tradesMatched: totalMatched,
503
- tradesUnmatched: unmatchedDates.length,
504
- tradesLagExcluded: lagExcluded,
505
- unmatchedDates: sortedUnmatchedDates,
506
- overall: {
507
- winRate: Math.round(overallWinRate * 100) / 100,
508
- avgPl: Math.round(overallAvgPl * 100) / 100,
509
- totalPl: Math.round(totalPl * 100) / 100,
510
- },
511
- segments: segmentStats,
512
- };
513
-
514
- if (availability.warnings.length > 0) {
515
- responseData.warnings = availability.warnings;
516
- }
519
+ const responseData: Record<string, unknown> = {
520
+ blockId,
521
+ segmentBy,
522
+ lagNote,
523
+ strategy: strategy || null,
524
+ tradesTotal: trades.length,
525
+ tradesMatched: totalMatched,
526
+ tradesUnmatched: unmatchedDates.length,
527
+ tradesLagExcluded: lagExcluded,
528
+ unmatchedDates: sortedUnmatchedDates,
529
+ overall: {
530
+ winRate: Math.round(overallWinRate * 100) / 100,
531
+ avgPl: Math.round(overallAvgPl * 100) / 100,
532
+ totalPl: Math.round(totalPl * 100) / 100,
533
+ },
534
+ segments: segmentStats,
535
+ };
517
536
 
518
- return createToolOutput(summary, responseData);
519
- } catch (error) {
520
- return {
521
- content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
522
- isError: true,
523
- };
524
- }
525
- }),
537
+ if (availability.warnings.length > 0) {
538
+ responseData.warnings = availability.warnings;
539
+ }
540
+
541
+ return createToolOutput(summary, responseData);
542
+ } catch (error) {
543
+ return {
544
+ content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
545
+ isError: true,
546
+ };
547
+ }
548
+ },
549
+ ),
526
550
  );
527
551
 
528
552
  // ---------------------------------------------------------------------------
@@ -534,11 +558,13 @@ export function registerMarketDataTools(
534
558
  description:
535
559
  "Analyze a block's losing trades and suggest market-based filters that would have improved performance. " +
536
560
  "Returns actionable standalone filter suggestions plus composite filters where cross-field correlations are strong. " +
561
+ "objective defaults to totalPl: only positive P&L improvements, ranked by P&L. winRate restores win-rate ranking with P&L tie-breaks. minImprovementPct is ignored under totalPl. " +
537
562
  "Close-derived fields (VIX_Close, Vol_Regime, RSI_14, Realized_Vol_5D/20D, VIX_IVR, VIX_IVP) use prior trading day values. " +
538
563
  "Open-known fields (Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Is_Opex) use same-day values. " +
539
564
  "Uses market.enriched + market.spot_daily LEFT JOIN VIX tickers + market.enriched_context. Returns warnings when market data is partially missing.",
540
565
  inputSchema: z.object({
541
566
  blockId: z.string().describe("Block ID to analyze"),
567
+ maxMissedSessions: maxMissedSessionsSchema,
542
568
  fields: z
543
569
  .array(z.string())
544
570
  .min(1)
@@ -547,6 +573,12 @@ export function registerMarketDataTools(
547
573
  "Sweep numeric thresholds or categorical exclusions for these built-in or dataset fields. Omit to keep default suggestions.",
548
574
  ),
549
575
  strategy: z.string().optional().describe("Filter to specific strategy"),
576
+ objective: z
577
+ .enum(["totalPl", "winRate"])
578
+ .default("totalPl")
579
+ .describe(
580
+ "Admission/ranking objective (default totalPl). minImprovementPct applies only to winRate.",
581
+ ),
550
582
  strategyName: z
551
583
  .string()
552
584
  .optional()
@@ -556,13 +588,24 @@ export function registerMarketDataTools(
556
588
  minImprovementPct: z
557
589
  .number()
558
590
  .optional()
559
- .describe("Only suggest filters with >= X% win rate improvement (default: 3)"),
591
+ .describe(
592
+ "Minimum win-rate gain in percentage points under winRate (default: 3); ignored under totalPl",
593
+ ),
560
594
  ticker: z.string().optional().describe("Underlying ticker symbol (default: SPX)"),
561
595
  }),
562
596
  },
563
597
  withFullSync(
564
598
  baseDir,
565
- async ({ blockId, strategy, strategyName, minImprovementPct = 3, ticker, fields }) => {
599
+ async ({
600
+ blockId,
601
+ strategy,
602
+ strategyName,
603
+ minImprovementPct,
604
+ ticker,
605
+ fields,
606
+ objective = "totalPl",
607
+ maxMissedSessions,
608
+ }) => {
566
609
  try {
567
610
  const block = await loadBlock(baseDir, blockId);
568
611
  let trades = block.trades;
@@ -605,7 +648,15 @@ export function registerMarketDataTools(
605
648
  }
606
649
 
607
650
  if (fields)
608
- return await suggestFieldFilters(baseDir, blockId, trades, fields, minImprovementPct);
651
+ return await suggestFieldFilters(
652
+ baseDir,
653
+ blockId,
654
+ trades,
655
+ fields,
656
+ minImprovementPct,
657
+ objective,
658
+ maxMissedSessions,
659
+ );
609
660
 
610
661
  // Collect unique trade keys (ticker+date) and query DuckDB for market data
611
662
  const tradeKeys = uniqueTradeLookupKeys(trades);
@@ -655,7 +706,7 @@ export function registerMarketDataTools(
655
706
  const currentTotalPl = matchedTrades.reduce((sum, t) => sum + t.trade.pl, 0);
656
707
 
657
708
  // Test various filters
658
- interface FilterSuggestion {
709
+ interface FilterSuggestion extends SuggestionEvidence {
659
710
  filter: string;
660
711
  condition: {
661
712
  field: string;
@@ -663,14 +714,12 @@ export function registerMarketDataTools(
663
714
  value: number | number[] | string;
664
715
  lagged: boolean;
665
716
  };
666
- tradesRemoved: number;
667
717
  winnersRemoved: number;
668
718
  losersRemoved: number;
669
719
  newWinRate: number;
670
720
  newTotalPl: number;
671
721
  winRateDelta: number;
672
722
  plDelta: number;
673
- confidence: "high" | "medium" | "low";
674
723
  }
675
724
 
676
725
  const suggestions: FilterSuggestion[] = [];
@@ -966,16 +1015,15 @@ export function registerMarketDataTools(
966
1015
  const winRateDelta = newWinRate - poolWinRate;
967
1016
  const plDelta = newTotalPl - poolTotalPl;
968
1017
 
969
- // Only include if improvement meets threshold
970
- if (winRateDelta >= minImprovementPct) {
971
- // Determine confidence based on sample size
972
- let confidence: "high" | "medium" | "low" = "low";
973
- if (removed.length >= 10 && remaining.length >= 20) {
974
- confidence = "high";
975
- } else if (removed.length >= 5 && remaining.length >= 10) {
976
- confidence = "medium";
977
- }
978
-
1018
+ const evidence = suggestionEvidence(
1019
+ objective,
1020
+ winRateDelta,
1021
+ plDelta,
1022
+ remaining.length,
1023
+ removed.length,
1024
+ minImprovementPct ?? 3,
1025
+ );
1026
+ if (evidence) {
979
1027
  suggestions.push({
980
1028
  filter: filterDef.name,
981
1029
  condition: {
@@ -984,35 +1032,34 @@ export function registerMarketDataTools(
984
1032
  value: filterDef.value,
985
1033
  lagged: filterDef.lagged,
986
1034
  },
987
- tradesRemoved: removed.length,
1035
+ ...evidence,
988
1036
  winnersRemoved,
989
1037
  losersRemoved,
990
1038
  newWinRate: Math.round(newWinRate * 100) / 100,
991
1039
  newTotalPl: Math.round(newTotalPl * 100) / 100,
992
1040
  winRateDelta: Math.round(winRateDelta * 100) / 100,
993
1041
  plDelta: Math.round(plDelta * 100) / 100,
994
- confidence,
995
1042
  });
996
1043
  }
997
1044
  }
998
1045
 
999
- // Sort by win rate improvement
1000
- suggestions.sort((a, b) => b.winRateDelta - a.winRateDelta);
1046
+ suggestions.sort((a, b) => rankSuggestions(objective, a, b));
1001
1047
 
1002
1048
  // Take top 10
1003
1049
  const topSuggestions = suggestions.slice(0, 10);
1004
1050
 
1005
1051
  // Generate composite filter suggestions from pairs of top-performing standalone filters
1006
- const baseWinRate = currentWinRate;
1007
- const significantFilters = suggestions.filter((s) => s.winRateDelta > 3);
1052
+ const significantFilters = topSuggestions;
1008
1053
 
1009
- interface CompositeSuggestion {
1054
+ interface CompositeSuggestion extends SuggestionEvidence {
1010
1055
  name: string;
1011
1056
  type: "composite";
1012
1057
  projectedWinRate: number;
1013
1058
  projectedAvgPl: number;
1014
1059
  tradesAffected: number;
1015
1060
  improvement: number;
1061
+ winRateDelta: number;
1062
+ plDelta: number;
1016
1063
  }
1017
1064
 
1018
1065
  const compositeSuggestions: CompositeSuggestion[] = [];
@@ -1050,45 +1097,70 @@ export function registerMarketDataTools(
1050
1097
  );
1051
1098
  }
1052
1099
 
1053
- // Find trades that match BOTH filters (would be skipped by both)
1054
- const bothMatchTrades = pool.filter((t) => {
1055
- const m = t.market as Record<string, unknown>;
1056
- return testA.test(m) && testB.test(m);
1057
- });
1058
-
1059
- if (bothMatchTrades.length < 5) continue;
1100
+ // Compare both standalone kept sets on the same covered pool as the composite.
1101
+ const bothMatchTrades: EnrichedTrade[] = [];
1102
+ let keptA = 0;
1103
+ let winsA = 0;
1104
+ let keptB = 0;
1105
+ let winsB = 0;
1106
+ for (const item of pool) {
1107
+ const market = item.market as Record<string, unknown>;
1108
+ const skipA = testA.test(market);
1109
+ const skipB = testB.test(market);
1110
+ if (skipA && skipB) bothMatchTrades.push(item);
1111
+ if (!skipA) {
1112
+ keptA++;
1113
+ if (item.trade.pl > 0) winsA++;
1114
+ }
1115
+ if (!skipB) {
1116
+ keptB++;
1117
+ if (item.trade.pl > 0) winsB++;
1118
+ }
1119
+ }
1120
+ if (!keptA || !keptB) continue;
1060
1121
 
1122
+ const excluded = new Set(bothMatchTrades);
1123
+ const kept = pool.filter((trade) => !excluded.has(trade));
1124
+ if (!kept.length || !bothMatchTrades.length) continue;
1061
1125
  const compositeWinRate =
1062
- (bothMatchTrades.filter((t) => t.trade.pl > 0).length / bothMatchTrades.length) *
1063
- 100;
1064
- const compositeAvgPl =
1065
- bothMatchTrades.reduce((sum, t) => sum + t.trade.pl, 0) / bothMatchTrades.length;
1066
-
1067
- // Only surface if composite win rate is materially better than either standalone filter alone
1068
- const improvement =
1069
- compositeWinRate -
1070
- Math.max(filterA.winRateDelta + baseWinRate, filterB.winRateDelta + baseWinRate);
1071
- if (improvement > 2) {
1126
+ (kept.filter((t) => t.trade.pl > 0).length / kept.length) * 100;
1127
+ const improvement = compositeWinRate - Math.max(winsA / keptA, winsB / keptB) * 100;
1128
+ const totalPl = kept.reduce((sum, t) => sum + t.trade.pl, 0);
1129
+ const winRateDelta =
1130
+ compositeWinRate - (pool.filter((t) => t.trade.pl > 0).length / pool.length) * 100;
1131
+ const plDelta = totalPl - pool.reduce((sum, t) => sum + t.trade.pl, 0);
1132
+ const evidence = suggestionEvidence(
1133
+ objective,
1134
+ winRateDelta,
1135
+ plDelta,
1136
+ kept.length,
1137
+ bothMatchTrades.length,
1138
+ minImprovementPct ?? 3,
1139
+ );
1140
+ if (evidence) {
1072
1141
  compositeSuggestions.push({
1073
1142
  name: `${filterA.filter} AND ${filterB.filter}`,
1074
1143
  type: "composite",
1075
1144
  projectedWinRate: Math.round(compositeWinRate * 100) / 100,
1076
- projectedAvgPl: Math.round(compositeAvgPl * 100) / 100,
1145
+ projectedAvgPl: Math.round((totalPl / kept.length) * 100) / 100,
1077
1146
  tradesAffected: bothMatchTrades.length,
1078
1147
  improvement: Math.round(improvement * 100) / 100,
1148
+ winRateDelta: Math.round(winRateDelta * 100) / 100,
1149
+ plDelta: Math.round(plDelta * 100) / 100,
1150
+ ...evidence,
1079
1151
  });
1080
1152
  }
1081
1153
  }
1082
1154
  }
1083
1155
 
1084
- // Sort composites by improvement, take top 5
1085
- compositeSuggestions.sort((a, b) => b.improvement - a.improvement);
1156
+ compositeSuggestions.sort((a, b) => rankSuggestions(objective, a, b));
1086
1157
  const topCompositeSuggestions = compositeSuggestions.slice(0, 5);
1087
1158
 
1088
- const summary = `Filter analysis: ${blockId} | ${topSuggestions.length} standalone, ${topCompositeSuggestions.length} composite suggestions (min ${minImprovementPct}% improvement)`;
1159
+ const summary = `Filter analysis: ${blockId} | objective ${objective} | ${topSuggestions.length} standalone, ${topCompositeSuggestions.length} composite suggestions`;
1089
1160
 
1090
1161
  const sfResponseData: Record<string, unknown> = {
1091
1162
  blockId,
1163
+ ...objectiveOutput(objective, minImprovementPct),
1092
1164
  lagNote:
1093
1165
  "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.",
1094
1166
  strategy: effectiveStrategy || null,
@@ -1100,7 +1172,7 @@ export function registerMarketDataTools(
1100
1172
  },
1101
1173
  suggestedFilters: topSuggestions,
1102
1174
  compositeSuggestions: topCompositeSuggestions,
1103
- minImprovementThreshold: minImprovementPct,
1175
+ minImprovementThreshold: objective === "winRate" ? (minImprovementPct ?? 3) : null,
1104
1176
  };
1105
1177
 
1106
1178
  if (availabilitySF.warnings.length > 0) {
@@ -1168,6 +1240,7 @@ export function registerMarketDataTools(
1168
1240
  "Returns warnings when market data is partially missing.",
1169
1241
  inputSchema: z.object({
1170
1242
  blockId: z.string().describe("Block ID to enrich"),
1243
+ maxMissedSessions: maxMissedSessionsSchema,
1171
1244
  strategy: z.string().optional().describe("Filter to specific strategy (exact match)"),
1172
1245
  startDate: z.string().optional().describe("Start date filter (YYYY-MM-DD)"),
1173
1246
  endDate: z.string().optional().describe("End date filter (YYYY-MM-DD)"),
@@ -1203,6 +1276,7 @@ export function registerMarketDataTools(
1203
1276
  includeIntradayContext,
1204
1277
  limit,
1205
1278
  offset,
1279
+ maxMissedSessions,
1206
1280
  }) => {
1207
1281
  try {
1208
1282
  const block = await loadBlock(baseDir, blockId);
@@ -1328,14 +1402,18 @@ export function registerMarketDataTools(
1328
1402
  const unmatchedDates: string[] = [];
1329
1403
  let matched = 0;
1330
1404
 
1331
- const enrichedTrades = paginated.map((trade) => {
1405
+ const datasetMatches = paginated.map((trade) =>
1406
+ resolveTradeDatasets(datasets, trade, maxMissedSessions),
1407
+ );
1408
+ const datasetAge = summarizeDatasetAge(paginated, datasetMatches);
1409
+ const enrichedTrades = paginated.map((trade, index) => {
1332
1410
  const lookup = getTradeLookupKey(trade);
1333
1411
  const marketKey = marketTickerDateKey(lookup.ticker, lookup.date);
1334
1412
  const marketData = daily.get(marketKey);
1335
1413
 
1336
1414
  const commissions = trade.openingCommissionsFees + trade.closingCommissionsFees;
1337
1415
 
1338
- const datasetMatch = resolveTradeDatasets(datasets, trade);
1416
+ const datasetMatch = datasetMatches[index];
1339
1417
  const baseTrade: Record<string, unknown> = {
1340
1418
  dateOpened: lookup.date,
1341
1419
  ticker: lookup.ticker,
@@ -1350,6 +1428,9 @@ export function registerMarketDataTools(
1350
1428
  customFields: trade.customFields ?? {},
1351
1429
  datasetFields: datasetMatch.values,
1352
1430
  datasetProvenance: datasetMatch.provenance,
1431
+ ...(Object.keys(datasetMatch.rejected).length
1432
+ ? { datasetRejections: datasetMatch.rejected }
1433
+ : {}),
1353
1434
  };
1354
1435
 
1355
1436
  if (!marketData) {
@@ -1418,6 +1499,7 @@ export function registerMarketDataTools(
1418
1499
  // Build response data
1419
1500
  const responseData: Record<string, unknown> = {
1420
1501
  blockId,
1502
+ ...datasetAgeOutput(datasetAge, Object.keys(datasetAge)),
1421
1503
  strategy: strategy || null,
1422
1504
  lagNote,
1423
1505
  tradesTotal: totalTrades,