tradeblocks-mcp 4.2.0 → 4.4.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 (48) hide show
  1. package/README.md +72 -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-62GCLMQA.js → chunk-CZNPQ574.js} +6 -9
  5. package/dist/chunk-CZNPQ574.js.map +1 -0
  6. package/dist/{chunk-4UU7IT2A.js → chunk-ENXBPJ7Y.js} +148 -6
  7. package/dist/chunk-ENXBPJ7Y.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-P6QUZFSF.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-J4GWE6JR.js → chunk-5JTQEV7P.js} +307 -255
  19. package/server/chunk-5JTQEV7P.js.map +1 -0
  20. package/server/{chunk-7N6HBOAY.js → chunk-PDSUY2X2.js} +148 -6
  21. package/server/chunk-PDSUY2X2.js.map +1 -0
  22. package/server/{daily-logs-store-YAXASQN4.js → daily-logs-store-LRVO7COL.js} +2 -2
  23. package/server/index.js +1326 -928
  24. package/server/index.js.map +1 -1
  25. package/server/{sync-ZV3QEGMO.js → sync-336543PJ.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 +177 -191
  33. package/src/tools/shared/field-analysis.ts +57 -14
  34. package/src/tools/shared/filter-objective.ts +73 -0
  35. package/src/utils/filter-curve.ts +257 -0
  36. package/src/utils/trade-analysis-fields.ts +19 -4
  37. package/dist/chunk-62GCLMQA.js.map +0 -1
  38. package/dist/chunk-YRD62LDE.js.map +0 -1
  39. package/server/chunk-7N6HBOAY.js.map +0 -1
  40. package/server/chunk-J4GWE6JR.js.map +0 -1
  41. /package/dist/{daily-logs-store-65YHFJ5F.js.map → daily-logs-store-FZRIGYUX.js.map} +0 -0
  42. /package/dist/{daily-logs-store-PXB53WEK.js.map → daily-logs-store-MEZ5S3BR.js.map} +0 -0
  43. /package/dist/{sync-JL7OPLCT.js.map → sync-P6QUZFSF.js.map} +0 -0
  44. /package/dist/{trades-store-MOFJHRC2.js.map → trades-store-3TOLBL2W.js.map} +0 -0
  45. /package/dist/{trades-store-UGZ62A7N.js.map → trades-store-PNLPRYRJ.js.map} +0 -0
  46. /package/server/{daily-logs-store-YAXASQN4.js.map → daily-logs-store-LRVO7COL.js.map} +0 -0
  47. /package/server/{sync-ZV3QEGMO.js.map → sync-336543PJ.js.map} +0 -0
  48. /package/server/{trades-store-MF7YYLCH.js.map → trades-store-MXUZLSF5.js.map} +0 -0
@@ -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
+ }
@@ -0,0 +1,257 @@
1
+ import { sweepThresholds } from "./threshold-sweep.ts";
2
+ import { rankSuggestions, type FilterObjective } from "../tools/shared/filter-objective.ts";
3
+
4
+ interface CurveTrade {
5
+ pl: number;
6
+ dateOpened: Date;
7
+ }
8
+ interface Options {
9
+ mode: "lt" | "gt" | "both";
10
+ thresholds?: number[];
11
+ percentileSteps?: number[];
12
+ objective?: FilterObjective;
13
+ permutation?: { iterations: number; block: "month"; seed: number };
14
+ }
15
+
16
+ function population(trades: Array<{ pl: number }>) {
17
+ const totalPl = trades.reduce((sum, trade) => sum + trade.pl, 0);
18
+ return {
19
+ count: trades.length,
20
+ totalPl,
21
+ winRate: trades.length ? trades.filter((trade) => trade.pl > 0).length / trades.length : 0,
22
+ };
23
+ }
24
+
25
+ /** The same sweep and objective ranking are used for observed and permuted outcomes. */
26
+ export function analyzeFilterCurve<T extends CurveTrade>(
27
+ trades: T[],
28
+ valueOf: (trade: T) => number | null,
29
+ options: Options,
30
+ ) {
31
+ const covered = trades.filter((trade) => valueOf(trade) !== null);
32
+ const base = population(covered);
33
+ const uncovered = population(trades.filter((trade) => valueOf(trade) === null));
34
+ const baseline = {
35
+ count: base.count,
36
+ winRate: Math.round(base.winRate * 10000) / 10000,
37
+ avgPl: Math.round((base.totalPl / base.count) * 100) / 100,
38
+ totalPl: Math.round(base.totalPl * 100) / 100,
39
+ uncoveredCount: uncovered.count,
40
+ uncoveredTotalPl: Math.round(uncovered.totalPl * 100) / 100,
41
+ };
42
+ function metrics(items: T[]) {
43
+ const stats = population(items);
44
+ const avgPl = stats.count ? stats.totalPl / stats.count : 0;
45
+ return {
46
+ count: stats.count,
47
+ percentOfTrades: Math.round((stats.count / base.count) * 10000) / 100,
48
+ winRate: Math.round(stats.winRate * 10000) / 10000,
49
+ avgPl: Math.round(avgPl * 100) / 100,
50
+ totalPl: Math.round(stats.totalPl * 100) / 100,
51
+ winRateDelta: stats.count
52
+ ? Math.round((stats.winRate - baseline.winRate) * 10000) / 10000
53
+ : -baseline.winRate,
54
+ avgPlDelta: stats.count ? Math.round((avgPl - baseline.avgPl) * 100) / 100 : -baseline.avgPl,
55
+ ...(stats.count > 0 && stats.count < 30
56
+ ? {
57
+ lowSampleWarning: `${stats.count} trades may be insufficient for reliable statistics (recommend >= 30)`,
58
+ }
59
+ : {}),
60
+ };
61
+ }
62
+ const sweeps = sweepThresholds(covered, valueOf, options.thresholds, options.percentileSteps);
63
+ const thresholds = sweeps.map((sweep) => ({
64
+ threshold: sweep.threshold,
65
+ ...(options.mode !== "gt" ? { lt: metrics(sweep.lt) } : {}),
66
+ ...(options.mode !== "lt" ? { gt: metrics(sweep.gt) } : {}),
67
+ }));
68
+ const sweetSpots = thresholds
69
+ .flatMap((row) =>
70
+ (["lt", "gt"] as const).flatMap((direction) => {
71
+ const stats = row[direction];
72
+ if (
73
+ !stats ||
74
+ stats.winRateDelta <= 0 ||
75
+ stats.avgPlDelta <= 0 ||
76
+ stats.percentOfTrades < 20
77
+ )
78
+ return [];
79
+ return [
80
+ {
81
+ threshold: row.threshold,
82
+ direction,
83
+ winRateDelta: stats.winRateDelta,
84
+ avgPlDelta: stats.avgPlDelta,
85
+ percentOfTrades: stats.percentOfTrades,
86
+ score: Math.round(stats.winRateDelta * stats.avgPlDelta * 10000) / 10000,
87
+ },
88
+ ];
89
+ }),
90
+ )
91
+ .sort((a, b) => b.score - a.score);
92
+ const legacy = {
93
+ tradesAnalyzed: covered.length,
94
+ baseline,
95
+ thresholds,
96
+ sweetSpots,
97
+ sweetSpotCriteria: {
98
+ winRateDelta: "> 0 (win rate improves)",
99
+ avgPlDelta: "> 0 (average P/L improves)",
100
+ percentOfTrades: ">= 20% (retains meaningful sample)",
101
+ scoreFormula: "winRateDelta * avgPlDelta (higher = better)",
102
+ },
103
+ };
104
+ if (!options.objective && !options.permutation) return { ...legacy, analysis: undefined };
105
+ const objective = options.objective ?? "totalPl";
106
+ const pairs = covered.map((trade) => ({ value: valueOf(trade)!, pl: trade.pl }));
107
+ function best(items: Array<{ value: number; pl: number }>) {
108
+ const candidates = sweepThresholds(
109
+ items,
110
+ (item) => item.value,
111
+ options.thresholds,
112
+ options.percentileSteps,
113
+ ).flatMap((sweep) =>
114
+ (["lt", "gt"] as const).flatMap((direction) => {
115
+ const kept = sweep[direction];
116
+ if (
117
+ (options.mode !== "both" && options.mode !== direction) ||
118
+ !kept.length ||
119
+ kept.length === items.length
120
+ )
121
+ return [];
122
+ const stats = population(kept);
123
+ return [
124
+ {
125
+ threshold: sweep.threshold,
126
+ direction,
127
+ kept,
128
+ plDelta: stats.totalPl - base.totalPl,
129
+ winRateDelta: (stats.winRate - base.winRate) * 100,
130
+ },
131
+ ];
132
+ }),
133
+ );
134
+ candidates.sort((a, b) => rankSuggestions(objective, a, b));
135
+ return candidates[0] ?? null;
136
+ }
137
+ const chosen = best(pairs);
138
+ const bestThreshold = chosen
139
+ ? {
140
+ threshold: chosen.threshold,
141
+ direction: chosen.direction,
142
+ kept: population(chosen.kept),
143
+ removed: population(
144
+ pairs.filter((pair) =>
145
+ chosen.direction === "lt"
146
+ ? pair.value >= chosen.threshold
147
+ : pair.value <= chosen.threshold,
148
+ ),
149
+ ),
150
+ objectiveDelta: objective === "totalPl" ? chosen.plDelta : chosen.winRateDelta,
151
+ }
152
+ : null;
153
+ const years = [...new Set(trades.map((trade) => trade.dateOpened.getFullYear()))].sort();
154
+ const perYear = chosen
155
+ ? years.map((year) => {
156
+ const annual = covered.filter((trade) => trade.dateOpened.getFullYear() === year);
157
+ const uncovered = population(
158
+ trades.filter(
159
+ (trade) => trade.dateOpened.getFullYear() === year && valueOf(trade) === null,
160
+ ),
161
+ );
162
+ const kept = annual.filter((trade) =>
163
+ chosen.direction === "lt"
164
+ ? valueOf(trade)! < chosen.threshold
165
+ : valueOf(trade)! > chosen.threshold,
166
+ );
167
+ const removed = annual.filter((trade) =>
168
+ chosen.direction === "lt"
169
+ ? valueOf(trade)! >= chosen.threshold
170
+ : valueOf(trade)! <= chosen.threshold,
171
+ );
172
+ const baseline = population(annual);
173
+ const stats = population(kept);
174
+ return {
175
+ year,
176
+ baseline: {
177
+ ...baseline,
178
+ uncoveredCount: uncovered.count,
179
+ uncoveredTotalPl: uncovered.totalPl,
180
+ },
181
+ kept: stats,
182
+ removed: population(removed),
183
+ objectiveDelta:
184
+ objective === "totalPl"
185
+ ? stats.totalPl - baseline.totalPl
186
+ : (stats.winRate - baseline.winRate) * 100,
187
+ ...(annual.length < 30 || kept.length < 30 || removed.length < 30
188
+ ? { lowSampleWarning: "Fewer than 30 covered, kept or removed entries" }
189
+ : {}),
190
+ };
191
+ })
192
+ : [];
193
+ let permutation:
194
+ | {
195
+ iterations: number;
196
+ block: "month";
197
+ seed: number;
198
+ blockCount: number;
199
+ status: string;
200
+ reason?: string;
201
+ pValue?: number;
202
+ exceedances?: number;
203
+ correction?: string;
204
+ }
205
+ | undefined;
206
+ if (options.permutation) {
207
+ const ordered = [...covered].sort((a, b) => a.dateOpened.getTime() - b.dateOpened.getTime());
208
+ const blocks = new Map<string, number[]>();
209
+ for (const trade of ordered) {
210
+ const key = `${trade.dateOpened.getFullYear()}-${trade.dateOpened.getMonth()}`;
211
+ if (!blocks.has(key)) blocks.set(key, []);
212
+ blocks.get(key)!.push(trade.pl);
213
+ }
214
+ permutation = { ...options.permutation, blockCount: blocks.size, status: "refused" };
215
+ if (covered.length < 30) permutation.reason = "too_few_covered_trades";
216
+ else if (blocks.size < 8) permutation.reason = "too_few_blocks";
217
+ else if (!chosen) permutation.reason = "no_eligible_threshold";
218
+ else {
219
+ let state = options.permutation.seed >>> 0;
220
+ const rng = () => {
221
+ state = (Math.imul(1664525, state) + 1013904223) >>> 0;
222
+ return state / 4294967296;
223
+ };
224
+ const original = [...blocks.values()];
225
+ let exceedances = 0;
226
+ const observed = objective === "totalPl" ? chosen.plDelta : chosen.winRateDelta;
227
+ const orderedValues = ordered.map((trade) => valueOf(trade)!);
228
+ for (let iteration = 0; iteration < options.permutation.iterations; iteration++) {
229
+ const shuffled = [...original];
230
+ for (let i = shuffled.length - 1; i > 0; i--) {
231
+ const j = Math.floor(rng() * (i + 1));
232
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
233
+ }
234
+ const outcomes = shuffled.flat();
235
+ const permuted = outcomes.map((pl, index) => ({ value: orderedValues[index], pl }));
236
+ const winner = best(permuted);
237
+ const score = winner
238
+ ? objective === "totalPl"
239
+ ? winner.plDelta
240
+ : winner.winRateDelta
241
+ : -Infinity;
242
+ if (score >= observed) exceedances++;
243
+ }
244
+ permutation = {
245
+ ...permutation,
246
+ status: "ok",
247
+ exceedances,
248
+ pValue: (exceedances + 1) / (options.permutation.iterations + 1),
249
+ correction: "add-one: (exceedances + 1) / (iterations + 1)",
250
+ };
251
+ }
252
+ }
253
+ return {
254
+ ...legacy,
255
+ analysis: { objective, bestThreshold, perYear, ...(permutation ? { permutation } : {}) },
256
+ };
257
+ }
@@ -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
  }