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.
- package/README.md +40 -1
- package/dist/{chunk-YRD62LDE.js → chunk-35RVWZGT.js} +4 -1
- package/dist/{chunk-4UU7IT2A.js.map → chunk-35RVWZGT.js.map} +1 -1
- package/dist/{chunk-4UU7IT2A.js → chunk-ENXBPJ7Y.js} +148 -6
- package/dist/chunk-ENXBPJ7Y.js.map +1 -0
- package/dist/{chunk-62GCLMQA.js → chunk-TLX7IBCM.js} +3 -6
- package/dist/chunk-TLX7IBCM.js.map +1 -0
- package/dist/{daily-logs-store-PXB53WEK.js → daily-logs-store-FZRIGYUX.js} +2 -2
- package/dist/{daily-logs-store-65YHFJ5F.js → daily-logs-store-MEZ5S3BR.js} +2 -2
- package/dist/market/provenance/index.js +1 -4
- package/dist/market/provenance/index.js.map +1 -1
- package/dist/{sync-JL7OPLCT.js → sync-EW33QZPD.js} +3 -3
- package/dist/test-exports.js +320 -279
- package/dist/test-exports.js.map +1 -1
- package/dist/{trades-store-UGZ62A7N.js → trades-store-3TOLBL2W.js} +2 -2
- package/dist/{trades-store-MOFJHRC2.js → trades-store-PNLPRYRJ.js} +2 -2
- package/package.json +1 -1
- package/server/{chunk-7N6HBOAY.js → chunk-PDSUY2X2.js} +148 -6
- package/server/chunk-PDSUY2X2.js.map +1 -0
- package/server/{chunk-J4GWE6JR.js → chunk-YIWLVX7Z.js} +3 -6
- package/server/chunk-YIWLVX7Z.js.map +1 -0
- package/server/{daily-logs-store-YAXASQN4.js → daily-logs-store-LRVO7COL.js} +2 -2
- package/server/index.js +993 -801
- package/server/index.js.map +1 -1
- package/server/{sync-ZV3QEGMO.js → sync-2POG6KA6.js} +3 -3
- package/server/{trades-store-MF7YYLCH.js → trades-store-MXUZLSF5.js} +2 -2
- package/src/db/dataset-schemas.ts +97 -23
- package/src/tools/market-data.ts +327 -245
- package/src/tools/profile-analysis.ts +18 -4
- package/src/tools/reports/fields.ts +16 -11
- package/src/tools/reports/helpers.ts +6 -2
- package/src/tools/reports/predictive.ts +15 -4
- package/src/tools/shared/field-analysis.ts +57 -14
- package/src/tools/shared/filter-objective.ts +73 -0
- package/src/utils/trade-analysis-fields.ts +19 -4
- package/dist/chunk-62GCLMQA.js.map +0 -1
- package/dist/chunk-YRD62LDE.js.map +0 -1
- package/server/chunk-7N6HBOAY.js.map +0 -1
- package/server/chunk-J4GWE6JR.js.map +0 -1
- /package/dist/{daily-logs-store-65YHFJ5F.js.map → daily-logs-store-FZRIGYUX.js.map} +0 -0
- /package/dist/{daily-logs-store-PXB53WEK.js.map → daily-logs-store-MEZ5S3BR.js.map} +0 -0
- /package/dist/{sync-JL7OPLCT.js.map → sync-EW33QZPD.js.map} +0 -0
- /package/dist/{trades-store-MOFJHRC2.js.map → trades-store-3TOLBL2W.js.map} +0 -0
- /package/dist/{trades-store-UGZ62A7N.js.map → trades-store-PNLPRYRJ.js.map} +0 -0
- /package/server/{daily-logs-store-YAXASQN4.js.map → daily-logs-store-LRVO7COL.js.map} +0 -0
- /package/server/{sync-ZV3QEGMO.js.map → sync-2POG6KA6.js.map} +0 -0
- /package/server/{trades-store-MF7YYLCH.js.map → trades-store-MXUZLSF5.js.map} +0 -0
package/src/tools/market-data.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
323
|
+
if (strategy) {
|
|
324
|
+
trades = trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
|
|
325
|
+
}
|
|
311
326
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
345
|
+
// Collect unique trade keys (ticker+date) and query DuckDB for market data
|
|
346
|
+
const tradeKeys = uniqueTradeLookupKeys(trades);
|
|
324
347
|
|
|
325
|
-
|
|
348
|
+
const conn = await getConnection(baseDir);
|
|
326
349
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
373
|
+
for (const trade of trades) {
|
|
374
|
+
const lookup = getTradeLookupKey(trade);
|
|
375
|
+
const marketData = daily.get(marketTickerDateKey(lookup.ticker, lookup.date));
|
|
353
376
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
377
|
+
if (!marketData) {
|
|
378
|
+
unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
358
381
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
segmentLabel = `Gap ${segmentValue}`;
|
|
403
|
-
break;
|
|
428
|
+
default:
|
|
429
|
+
continue;
|
|
404
430
|
}
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
423
|
-
}
|
|
508
|
+
const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
|
|
424
509
|
|
|
425
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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(
|
|
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 ({
|
|
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(
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
1054
|
-
const bothMatchTrades =
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
(
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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,
|