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
@@ -35,6 +35,8 @@ import {
35
35
  runtimeFieldCatalog,
36
36
  summarizeDatasetAge,
37
37
  datasetAgeOutput,
38
+ maxMissedSessionsSchema,
39
+ type DatasetProvenance,
38
40
  } from "../db/dataset-schemas.ts";
39
41
 
40
42
  // =============================================================================
@@ -177,7 +179,8 @@ interface TradeWithFields {
177
179
  trade: Trade;
178
180
  market: Record<string, unknown>;
179
181
  builtIn: Record<string, unknown> | undefined;
180
- provenance: Record<string, { sourceTimestamp: string; availableAt: string }>;
182
+ provenance: Record<string, DatasetProvenance>;
183
+ rejected: Record<string, DatasetProvenance>;
181
184
  }
182
185
 
183
186
  function partitionMarketFilters(entryFilters: EntryFilter[], catalog: { field: string }[]) {
@@ -212,6 +215,7 @@ async function loadTradesAndMarket(
212
215
  baseDir: string,
213
216
  blockId: string,
214
217
  strategyName: string,
218
+ maxMissedSessions?: number,
215
219
  ): Promise<{
216
220
  rows: TradeWithFields[];
217
221
  unmatchedCount: number;
@@ -244,7 +248,9 @@ async function loadTradesAndMarket(
244
248
  const marketRecords = resultToRecords(result);
245
249
  const marketMap = recordsByTickerDate(marketRecords);
246
250
  const datasets = await listDatasets(conn);
247
- const datasetMatches = trades.map((trade) => resolveTradeDatasets(datasets, trade));
251
+ const datasetMatches = trades.map((trade) =>
252
+ resolveTradeDatasets(datasets, trade, maxMissedSessions),
253
+ );
248
254
 
249
255
  // Preserve built-in coverage separately from the per-field joined values.
250
256
  const rows: TradeWithFields[] = [];
@@ -260,6 +266,7 @@ async function loadTradesAndMarket(
260
266
  builtIn,
261
267
  market: { ...builtIn, ...datasetValues },
262
268
  provenance: datasetMatches[index].provenance,
269
+ rejected: datasetMatches[index].rejected,
263
270
  });
264
271
  if (!builtIn) unmatchedCount++;
265
272
  }
@@ -330,6 +337,7 @@ function findBucket(
330
337
  export const analyzeStructureFitSchema = z.object({
331
338
  blockId: z.string().describe("Block ID to analyze"),
332
339
  strategyName: z.string().describe("Strategy name matching a stored profile"),
340
+ maxMissedSessions: maxMissedSessionsSchema,
333
341
  minTrades: z
334
342
  .number()
335
343
  .optional()
@@ -359,6 +367,7 @@ export async function handleAnalyzeStructureFit(
359
367
  baseDir,
360
368
  blockId,
361
369
  strategyName,
370
+ input.maxMissedSessions,
362
371
  );
363
372
 
364
373
  const catalog = await runtimeFieldCatalog(conn);
@@ -382,6 +391,7 @@ export async function handleAnalyzeStructureFit(
382
391
  return createToolOutput(`Uncovered dataset fields: ${uncovered.join(", ")}`, {
383
392
  error: "uncovered_fields",
384
393
  fields: uncovered,
394
+ ...datasetAgeOutput(datasetAge, uncovered),
385
395
  warnings,
386
396
  });
387
397
 
@@ -611,6 +621,7 @@ export async function handleAnalyzeStructureFit(
611
621
  export const validateEntryFiltersSchema = z.object({
612
622
  blockId: z.string().describe("Block ID to analyze"),
613
623
  strategyName: z.string().describe("Strategy name matching a stored profile"),
624
+ maxMissedSessions: maxMissedSessionsSchema,
614
625
  minTrades: z
615
626
  .number()
616
627
  .optional()
@@ -675,6 +686,7 @@ export async function handleValidateEntryFilters(
675
686
  baseDir,
676
687
  blockId,
677
688
  strategyName,
689
+ input.maxMissedSessions,
678
690
  );
679
691
  const predicates: FilterPredicate[] = filters.map((filter) =>
680
692
  buildFilterPredicate(filter, catalog),
@@ -682,9 +694,10 @@ export async function handleValidateEntryFilters(
682
694
  const matched = rows.filter(({ market }) =>
683
695
  predicates.every((predicate) => predicate.hasData(market)),
684
696
  );
697
+ const ageRows = input.maxMissedSessions === undefined ? matched : rows;
685
698
  const datasetAge = summarizeDatasetAge(
686
- matched.map(({ trade }) => trade),
687
- matched,
699
+ ageRows.map(({ trade }) => trade),
700
+ ageRows,
688
701
  );
689
702
  const uncovered = filters
690
703
  .filter(
@@ -697,6 +710,7 @@ export async function handleValidateEntryFilters(
697
710
  return createToolOutput(`Uncovered dataset fields: ${uncovered.join(", ")}`, {
698
711
  error: "uncovered_fields",
699
712
  fields: uncovered,
713
+ ...datasetAgeOutput(datasetAge, uncovered),
700
714
  warnings,
701
715
  });
702
716
 
@@ -14,7 +14,7 @@ import { createToolOutput } from "../../utils/output-formatter.ts";
14
14
  import { filterByStrategy, filterByDateRange } from "../shared/filters.ts";
15
15
  import { enrichAnalysisTrades } from "./helpers.ts";
16
16
  import { withSyncedBlock } from "../middleware/sync-middleware.ts";
17
- import { datasetAgeOutput } from "../../db/dataset-schemas.ts";
17
+ import { datasetAgeOutput, maxMissedSessionsSchema } from "../../db/dataset-schemas.ts";
18
18
 
19
19
  /**
20
20
  * Register field-related report tools
@@ -28,6 +28,7 @@ export function registerFieldTools(server: McpServer, baseDir: string): void {
28
28
  "Get detailed statistics for a specific field including min/max/avg/median/stdDev, percentiles, and histogram",
29
29
  inputSchema: z.object({
30
30
  blockId: z.string().describe("Block folder name"),
31
+ maxMissedSessions: maxMissedSessionsSchema,
31
32
  field: z
32
33
  .string()
33
34
  .describe(
@@ -46,7 +47,15 @@ export function registerFieldTools(server: McpServer, baseDir: string): void {
46
47
  },
47
48
  withSyncedBlock(
48
49
  baseDir,
49
- async ({ blockId, field, strategy, startDate, endDate, histogramBuckets }) => {
50
+ async ({
51
+ blockId,
52
+ field,
53
+ strategy,
54
+ startDate,
55
+ endDate,
56
+ histogramBuckets,
57
+ maxMissedSessions,
58
+ }) => {
50
59
  try {
51
60
  const block = await loadBlock(baseDir, blockId);
52
61
  let trades = block.trades;
@@ -67,7 +76,7 @@ export function registerFieldTools(server: McpServer, baseDir: string): void {
67
76
  }
68
77
 
69
78
  // Enrich trades
70
- const resolved = await enrichAnalysisTrades(baseDir, trades);
79
+ const resolved = await enrichAnalysisTrades(baseDir, trades, maxMissedSessions);
71
80
  const enrichedTrades = resolved.trades;
72
81
 
73
82
  // Extract field values
@@ -80,14 +89,10 @@ export function registerFieldTools(server: McpServer, baseDir: string): void {
80
89
  }
81
90
 
82
91
  if (values.length === 0) {
83
- return {
84
- content: [
85
- {
86
- type: "text",
87
- text: `Field "${field}" has no valid numeric values in the filtered trades.`,
88
- },
89
- ],
90
- };
92
+ return createToolOutput(
93
+ `Field "${field}" has no valid numeric values in the filtered trades.`,
94
+ { blockId, field, ...datasetAgeOutput(resolved.datasetAge, [field]) },
95
+ );
91
96
  }
92
97
 
93
98
  const analysis = fieldStatisticsFromValues(values, histogramBuckets)!;
@@ -52,8 +52,12 @@ export interface EnrichedTrade extends Trade {
52
52
  staticDatasetFields?: Record<string, Record<string, number | string>>;
53
53
  }
54
54
 
55
- export async function enrichAnalysisTrades(baseDir: string, trades: Trade[]) {
56
- const resolved = await resolveAnalysisFields(baseDir, trades);
55
+ export async function enrichAnalysisTrades(
56
+ baseDir: string,
57
+ trades: Trade[],
58
+ maxMissedSessions?: number,
59
+ ) {
60
+ const resolved = await resolveAnalysisFields(baseDir, trades, maxMissedSessions);
57
61
  const enriched = enrichTrades(trades);
58
62
  for (let i = 0; i < enriched.length; i++) {
59
63
  const datasets: Record<string, Record<string, number | string>> = {};
@@ -16,7 +16,7 @@ import { getConnection } from "../../db/connection.ts";
16
16
  import { getProfile } from "../../db/profile-schemas.ts";
17
17
  import { requireCoveredField } from "../../utils/trade-analysis-fields.ts";
18
18
  import { sweepThresholds } from "../../utils/threshold-sweep.ts";
19
- import { datasetAgeOutput } from "../../db/dataset-schemas.ts";
19
+ import { datasetAgeOutput, maxMissedSessionsSchema } from "../../db/dataset-schemas.ts";
20
20
 
21
21
  /**
22
22
  * Register predictive-related report tools
@@ -30,6 +30,7 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
30
30
  "Identify which trade entry conditions predict profitability by calculating Pearson correlations between all numeric fields and a target field (usually P/L). Returns fields ranked by predictive strength.",
31
31
  inputSchema: z.object({
32
32
  blockId: z.string().describe("Block folder name"),
33
+ maxMissedSessions: maxMissedSessionsSchema,
33
34
  strategy: z.string().optional().describe("Pre-filter by strategy name (case-insensitive)"),
34
35
  strategyName: z
35
36
  .string()
@@ -67,6 +68,7 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
67
68
  targetField,
68
69
  minSamples,
69
70
  includeCustomFields,
71
+ maxMissedSessions,
70
72
  }) => {
71
73
  try {
72
74
  const block = await loadBlock(baseDir, blockId);
@@ -96,7 +98,7 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
96
98
  }
97
99
 
98
100
  // Enrich trades
99
- const resolved = await enrichAnalysisTrades(baseDir, trades);
101
+ const resolved = await enrichAnalysisTrades(baseDir, trades, maxMissedSessions);
100
102
  const enrichedTrades = resolved.trades;
101
103
 
102
104
  const {
@@ -187,6 +189,7 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
187
189
  "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.",
188
190
  inputSchema: z.object({
189
191
  blockId: z.string().describe("Block folder name"),
192
+ maxMissedSessions: maxMissedSessionsSchema,
190
193
  field: z
191
194
  .string()
192
195
  .describe("Field to sweep thresholds on (e.g., 'openingVix', 'durationHours')"),
@@ -224,6 +227,7 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
224
227
  strategy,
225
228
  startDate,
226
229
  endDate,
230
+ maxMissedSessions,
227
231
  }) => {
228
232
  try {
229
233
  const block = await loadBlock(baseDir, blockId);
@@ -245,13 +249,13 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
245
249
  }
246
250
 
247
251
  // Enrich trades
248
- const resolved = await enrichAnalysisTrades(baseDir, trades);
252
+ const resolved = await enrichAnalysisTrades(baseDir, trades, maxMissedSessions);
249
253
  const enrichedTrades = resolved.trades;
250
254
  if (
251
255
  resolved.catalog.some((entry) => entry.field === field) ||
252
256
  (field.includes(".") && !field.startsWith("custom.") && !field.startsWith("daily."))
253
257
  )
254
- requireCoveredField(field, resolved.catalog, resolved.values);
258
+ requireCoveredField(field, resolved.catalog, resolved.values, resolved.datasetAge);
255
259
 
256
260
  // Extract field values
257
261
  const fieldValues: number[] = [];
@@ -288,6 +292,13 @@ export function registerPredictiveTools(server: McpServer, baseDir: string): voi
288
292
  winRate: Math.round(baselineWinRate * 10000) / 10000,
289
293
  avgPl: Math.round(baselineAvgPl * 100) / 100,
290
294
  totalPl: Math.round(baselineTotalPl * 100) / 100,
295
+ uncoveredCount: trades.length - tradesWithField.length,
296
+ uncoveredTotalPl:
297
+ Math.round(
298
+ enrichedTrades
299
+ .filter((trade) => getTradeFieldValue(trade, field) === null)
300
+ .reduce((sum, trade) => sum + trade.pl, 0) * 100,
301
+ ) / 100,
291
302
  };
292
303
 
293
304
  const sweeps = sweepThresholds(
@@ -4,35 +4,63 @@ import { sweepThresholds } from "../../utils/threshold-sweep.ts";
4
4
  import { computeSliceStats } from "../../utils/analysis-stats.ts";
5
5
  import { createToolOutput } from "../../utils/output-formatter.ts";
6
6
  import { datasetAgeOutput } from "../../db/dataset-schemas.ts";
7
+ import {
8
+ suggestionEvidence,
9
+ rankSuggestions,
10
+ objectiveOutput,
11
+ type FilterObjective,
12
+ type SuggestionEvidence,
13
+ } from "./filter-objective.ts";
7
14
 
8
15
  export async function suggestFieldFilters(
9
16
  baseDir: string,
10
17
  blockId: string,
11
18
  trades: Trade[],
12
19
  fields: string[],
13
- minImprovementPct: number,
20
+ minImprovementPct: number | undefined,
21
+ objective: FilterObjective,
22
+ maxMissedSessions?: number,
14
23
  ) {
15
- const { catalog, values, datasetAge } = await resolveAnalysisFields(baseDir, trades);
16
- const suggestions: {
24
+ const { catalog, values, datasetAge } = await resolveAnalysisFields(
25
+ baseDir,
26
+ trades,
27
+ maxMissedSessions,
28
+ );
29
+ const suggestions: (SuggestionEvidence & {
17
30
  filter: string;
18
31
  condition: { field: string; operator: string; value: number | string; lagged: boolean };
19
- tradesRemoved: number;
20
32
  winnersRemoved: number;
21
33
  losersRemoved: number;
22
34
  newWinRate: number;
23
35
  newTotalPl: number;
24
36
  winRateDelta: number;
25
37
  plDelta: number;
26
- confidence: string;
27
- }[] = [];
38
+ })[] = [];
28
39
  const coverage: Record<string, { covered: number; uncovered: number }> = {};
40
+ const fieldBaselines: Record<
41
+ string,
42
+ {
43
+ count: number;
44
+ totalPl: number;
45
+ uncoveredCount: number;
46
+ uncoveredTotalPl: number;
47
+ }
48
+ > = {};
29
49
  for (const field of fields) {
30
- requireCoveredField(field, catalog, values);
50
+ requireCoveredField(field, catalog, values, datasetAge);
31
51
  const pool = trades
32
52
  .map((trade, i) => ({ trade, value: values[i][field] }))
33
53
  .filter((item) => item.value !== undefined);
34
54
  coverage[field] = { covered: pool.length, uncovered: trades.length - pool.length };
35
55
  const baseline = computeSliceStats(pool.map(({ trade }) => trade.pl));
56
+ fieldBaselines[field] = {
57
+ count: pool.length,
58
+ totalPl: baseline.totalPl,
59
+ uncoveredCount: trades.length - pool.length,
60
+ uncoveredTotalPl: computeSliceStats(
61
+ trades.filter((_, i) => values[i][field] === undefined).map((trade) => trade.pl),
62
+ ).totalPl,
63
+ };
36
64
  const entry = catalog.find((entry) => entry.field === field)!;
37
65
  const candidates =
38
66
  entry.type === "numeric"
@@ -53,7 +81,15 @@ export async function suggestFieldFilters(
53
81
  if (!kept.length || !excluded.size) continue;
54
82
  const stats = computeSliceStats(kept.map(({ trade }) => trade.pl));
55
83
  const improvement = stats.winRate - baseline.winRate;
56
- if (improvement < minImprovementPct) continue;
84
+ const evidence = suggestionEvidence(
85
+ objective,
86
+ improvement,
87
+ stats.totalPl - baseline.totalPl,
88
+ kept.length,
89
+ excluded.size,
90
+ minImprovementPct ?? 3,
91
+ );
92
+ if (!evidence) continue;
57
93
  const winnersRemoved = candidate.excluded.filter(({ trade }) => trade.pl > 0).length;
58
94
  suggestions.push({
59
95
  filter: `Skip when ${field} ${candidate.operator} ${candidate.value}`,
@@ -63,27 +99,29 @@ export async function suggestFieldFilters(
63
99
  value: candidate.value,
64
100
  lagged: entry.timing === "close",
65
101
  },
66
- tradesRemoved: excluded.size,
102
+ ...evidence,
67
103
  winnersRemoved,
68
104
  losersRemoved: excluded.size - winnersRemoved,
69
105
  newWinRate: stats.winRate,
70
106
  newTotalPl: stats.totalPl,
71
107
  winRateDelta: improvement,
72
108
  plDelta: stats.totalPl - baseline.totalPl,
73
- confidence: kept.length >= 100 ? "high" : kept.length >= 30 ? "medium" : "low",
74
109
  });
75
110
  }
76
111
  }
77
- suggestions.sort((a, b) => b.winRateDelta - a.winRateDelta || b.plDelta - a.plDelta);
112
+ suggestions.sort((a, b) => rankSuggestions(objective, a, b));
78
113
  return createToolOutput(`Field filter analysis: ${blockId} | ${suggestions.length} suggestions`, {
79
114
  blockId,
80
115
  fields,
116
+ ...objectiveOutput(objective, minImprovementPct),
81
117
  currentStats: computeSliceStats(trades.map((trade) => trade.pl)),
82
118
  suggestedFilters: suggestions,
83
119
  compositeSuggestions: [],
84
120
  coverage,
121
+ fieldBaselines,
122
+ comparisonPopulation: "fieldBaselines",
85
123
  ...datasetAgeOutput(datasetAge, fields),
86
- minImprovementThreshold: minImprovementPct,
124
+ minImprovementThreshold: objective === "winRate" ? (minImprovementPct ?? 3) : null,
87
125
  });
88
126
  }
89
127
 
@@ -93,9 +131,14 @@ export async function analyzeFieldRegimes(
93
131
  trades: Trade[],
94
132
  field: string,
95
133
  bins?: number[],
134
+ maxMissedSessions?: number,
96
135
  ) {
97
- const { catalog, values, datasetAge } = await resolveAnalysisFields(baseDir, trades);
98
- requireCoveredField(field, catalog, values);
136
+ const { catalog, values, datasetAge } = await resolveAnalysisFields(
137
+ baseDir,
138
+ trades,
139
+ maxMissedSessions,
140
+ );
141
+ requireCoveredField(field, catalog, values, datasetAge);
99
142
  const entry = catalog.find((entry) => entry.field === field)!;
100
143
  if (bins && entry.type !== "numeric") throw new Error(`Bins require a numeric field: ${field}`);
101
144
  if (bins?.some((value, i) => !Number.isFinite(value) || (i > 0 && value <= bins[i - 1])))
@@ -0,0 +1,73 @@
1
+ export type FilterObjective = "totalPl" | "winRate";
2
+
3
+ export interface SuggestionEvidence {
4
+ objective: FilterObjective;
5
+ tradesKept: number;
6
+ keptFraction: number;
7
+ tradesRemoved: number;
8
+ confidence: "high" | "medium" | "low";
9
+ warnings: string[];
10
+ }
11
+
12
+ /** Both suggestion paths use the same admission and sample-size support rule. */
13
+ export function suggestionEvidence(
14
+ objective: FilterObjective,
15
+ winRateDelta: number,
16
+ plDelta: number,
17
+ kept: number,
18
+ removed: number,
19
+ minImprovementPct: number,
20
+ ): SuggestionEvidence | null {
21
+ if (
22
+ !kept ||
23
+ !removed ||
24
+ (objective === "totalPl" ? plDelta <= 0 : winRateDelta < minImprovementPct)
25
+ )
26
+ return null;
27
+ const keptFraction = kept / (kept + removed);
28
+ const lowRetention = keptFraction < 0.2;
29
+ return {
30
+ objective,
31
+ tradesKept: kept,
32
+ keptFraction,
33
+ tradesRemoved: removed,
34
+ confidence: lowRetention
35
+ ? "low"
36
+ : removed >= 10 && kept >= 20
37
+ ? "high"
38
+ : removed >= 5 && kept >= 10
39
+ ? "medium"
40
+ : "low",
41
+ warnings: lowRetention
42
+ ? ["Low retention: fewer than 20% of covered trades kept; sample-size support capped at low."]
43
+ : [],
44
+ };
45
+ }
46
+
47
+ export function rankSuggestions(
48
+ objective: FilterObjective,
49
+ a: { winRateDelta: number; plDelta: number },
50
+ b: { winRateDelta: number; plDelta: number },
51
+ ): number {
52
+ return objective === "totalPl"
53
+ ? b.plDelta - a.plDelta || b.winRateDelta - a.winRateDelta
54
+ : b.winRateDelta - a.winRateDelta || b.plDelta - a.plDelta;
55
+ }
56
+
57
+ export function objectiveOutput(objective: FilterObjective, minImprovementPct?: number) {
58
+ return {
59
+ objective,
60
+ rankingRule:
61
+ objective === "totalPl"
62
+ ? "Positive P&L improvement, ranked by P&L improvement then win-rate gain"
63
+ : "Win-rate gain >= minImprovementPct, ranked by win-rate gain then P&L improvement",
64
+ supportNote:
65
+ "confidence is sample-size support, not statistical confidence; keptFraction < 0.2 caps it at low",
66
+ ...(objective === "totalPl" && minImprovementPct !== undefined
67
+ ? {
68
+ objectiveNote:
69
+ "minImprovementPct is ignored under totalPl; only positive P&L improvement is admitted.",
70
+ }
71
+ : {}),
72
+ };
73
+ }
@@ -5,15 +5,22 @@ import {
5
5
  resolveTradeDatasets,
6
6
  runtimeFieldCatalog,
7
7
  summarizeDatasetAge,
8
+ type DatasetFieldAge,
8
9
  } from "../db/dataset-schemas.ts";
9
10
  import { buildLookaheadFreeQuery, CLOSE_KNOWN_FIELDS } from "./field-timing.ts";
10
11
  import { marketTickerDateKey, resolveTradeTicker } from "./ticker.ts";
11
12
 
12
- export async function resolveAnalysisFields(baseDir: string, trades: Trade[]) {
13
+ export async function resolveAnalysisFields(
14
+ baseDir: string,
15
+ trades: Trade[],
16
+ maxMissedSessions?: number,
17
+ ) {
13
18
  const conn = await getConnection(baseDir);
14
19
  const datasets = await listDatasets(conn);
15
20
  const catalog = await runtimeFieldCatalog(conn);
16
- const datasetMatches = trades.map((trade) => resolveTradeDatasets(datasets, trade));
21
+ const datasetMatches = trades.map((trade) =>
22
+ resolveTradeDatasets(datasets, trade, maxMissedSessions),
23
+ );
17
24
  const keys = trades.map((trade) => {
18
25
  const date = new Date(trade.dateOpened);
19
26
  const midnightUtc = date.getUTCHours() === 0 && date.getUTCMinutes() === 0;
@@ -64,9 +71,17 @@ export function requireCoveredField(
64
71
  field: string,
65
72
  catalog: { field: string }[],
66
73
  values: Record<string, number | string>[],
74
+ ages?: Record<string, DatasetFieldAge>,
67
75
  ) {
68
76
  if (!catalog.some((entry) => entry.field === field))
69
77
  throw new Error(`Unknown analysis field: ${field}`);
70
- if (!values.some((row) => row[field] !== undefined))
71
- throw new Error(`Uncovered analysis field: ${field}; no value available at trade entry`);
78
+ if (!values.some((row) => row[field] !== undefined)) {
79
+ const age = ages?.[field];
80
+ const reason = age?.rejectedAttachments
81
+ ? `; ${age.rejectedAttachments} refused by maxMissedSessions; ${age.unsupportedSessionAge} session ages unsupported`
82
+ : "";
83
+ throw new Error(
84
+ `Uncovered analysis field: ${field}; no value available at trade entry${reason}`,
85
+ );
86
+ }
72
87
  }