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
|
@@ -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,
|
|
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) =>
|
|
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
|
-
|
|
687
|
-
|
|
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 ({
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
56
|
-
|
|
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(
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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(
|
|
98
|
-
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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
|
}
|