tradeblocks-mcp 3.0.2 → 3.3.1
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 +85 -80
- package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
- package/dist/chunk-27S67XW3.js.map +1 -0
- package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
- package/dist/chunk-2E63THNI.js.map +1 -0
- package/dist/{chunk-QTTR7AAW.js → chunk-2I3S2ZLT.js} +325 -84
- package/dist/chunk-2I3S2ZLT.js.map +1 -0
- package/dist/{chunk-4BLCXNQ6.js → chunk-4HZLVUOZ.js} +179 -183
- package/dist/chunk-4HZLVUOZ.js.map +1 -0
- package/dist/{chunk-XXYOUIZY.js → chunk-NWNSKALN.js} +63 -80
- package/dist/chunk-NWNSKALN.js.map +1 -0
- package/dist/{chunk-PRAYH3RT.js → chunk-WNTYA7ER.js} +29 -12
- package/dist/chunk-WNTYA7ER.js.map +1 -0
- package/dist/{chunk-BKQ4PM4Y.js → chunk-YZA4RS76.js} +24 -18
- package/dist/chunk-YZA4RS76.js.map +1 -0
- package/dist/{chunk-W2PP3LEH.js → chunk-ZTJXC3LS.js} +17 -8
- package/dist/chunk-ZTJXC3LS.js.map +1 -0
- package/dist/daily-log-processor-MZW2XBZY.js +9 -0
- package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
- package/dist/iv-solver-worker.js +9 -1
- package/dist/market-provider-6U33TQUT.js +16 -0
- package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
- package/dist/{sync-PO4IPCYV.js → sync-3UBC37VW.js} +7 -7
- package/dist/test-exports.js +876 -483
- package/dist/test-exports.js.map +1 -1
- package/dist/trade-processor-DKDCWGKD.js +9 -0
- package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
- package/package.json +2 -6
- package/server/{chunk-CSDVJPBB.js → chunk-536NSFED.js} +29 -12
- package/server/chunk-536NSFED.js.map +1 -0
- package/server/{chunk-WA5AAPCH.js → chunk-A3UKD64A.js} +17 -8
- package/server/chunk-A3UKD64A.js.map +1 -0
- package/server/{chunk-PNKG7RY7.js → chunk-OMUDNJBJ.js} +63 -80
- package/server/chunk-OMUDNJBJ.js.map +1 -0
- package/server/{chunk-72GKJE2U.js → chunk-RJDJHZ5Y.js} +934 -335
- package/server/chunk-RJDJHZ5Y.js.map +1 -0
- package/server/{chunk-NZO6PT64.js → chunk-RM4B2VKS.js} +167 -177
- package/server/chunk-RM4B2VKS.js.map +1 -0
- package/server/{chunk-5EBXHT6C.js → chunk-SY6GDNVI.js} +24 -18
- package/server/chunk-SY6GDNVI.js.map +1 -0
- package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
- package/server/chunk-T66KH2XH.js.map +1 -0
- package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
- package/server/chunk-W5E7FHC4.js.map +1 -0
- package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
- package/server/config-DK7KOMNL.js.map +1 -0
- package/server/daily-log-processor-B2VPU2FQ.js +10 -0
- package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
- package/server/http-server.js +2 -5
- package/server/index.js +2497 -2029
- package/server/index.js.map +1 -1
- package/server/iv-solver-worker.js +9 -1
- package/server/market-provider-L56RFHAC.js +17 -0
- package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
- package/server/{sync-QFI5L7S7.js → sync-CENKDN53.js} +7 -7
- package/server/trade-processor-3PI4LHZI.js +10 -0
- package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
- package/src/auth/clients-store.ts +4 -4
- package/src/auth/code-store.ts +1 -1
- package/src/auth/config.ts +11 -12
- package/src/auth/login-page.ts +8 -10
- package/src/auth/provider.ts +35 -30
- package/src/auth/token.ts +17 -15
- package/src/db/backtest-schemas.ts +38 -8
- package/src/db/connection.ts +107 -40
- package/src/db/data-root.ts +22 -0
- package/src/db/index.ts +37 -5
- package/src/db/json-adapters.ts +22 -31
- package/src/db/json-migration.ts +11 -15
- package/src/db/json-store.ts +5 -3
- package/src/db/market-datasets.ts +48 -15
- package/src/db/market-schemas.ts +11 -5
- package/src/db/market-views.ts +119 -32
- package/src/db/parquet-writer.ts +7 -7
- package/src/db/profile-schemas.ts +39 -22
- package/src/db/schemas.ts +2 -2
- package/src/http-server.ts +7 -12
- package/src/index.ts +8 -9
- package/src/market/ingestor/index.ts +1 -4
- package/src/market/ingestor/market-ingestor.ts +140 -67
- package/src/market/ingestor/types.ts +1 -3
- package/src/market/stores/chain-sql.ts +2 -8
- package/src/market/stores/chain-store.ts +1 -4
- package/src/market/stores/duckdb-chain-store.ts +3 -14
- package/src/market/stores/duckdb-enriched-store.ts +3 -13
- package/src/market/stores/duckdb-quote-store.ts +10 -27
- package/src/market/stores/duckdb-spot-store.ts +4 -20
- package/src/market/stores/enriched-sql.ts +3 -9
- package/src/market/stores/enriched-store.ts +2 -2
- package/src/market/stores/parquet-chain-store.ts +5 -22
- package/src/market/stores/parquet-enriched-store.ts +4 -16
- package/src/market/stores/parquet-oi-daily-store.ts +3 -14
- package/src/market/stores/parquet-quote-store.ts +9 -29
- package/src/market/stores/parquet-spot-store.ts +14 -33
- package/src/market/stores/quote-store.ts +2 -10
- package/src/market/stores/spot-sql.ts +3 -15
- package/src/market/stores/spot-store.ts +1 -1
- package/src/market/tickers/defaults.json +6 -6
- package/src/market/tickers/loader.ts +2 -7
- package/src/market/tickers/registry.ts +5 -15
- package/src/market/tickers/schemas.ts +2 -6
- package/src/models/strategy-profile.ts +39 -39
- package/src/sync/block-sync.ts +44 -65
- package/src/sync/index.ts +1 -4
- package/src/sync/metadata.ts +14 -20
- package/src/test-exports.ts +139 -123
- package/src/tools/analysis.ts +180 -159
- package/src/tools/batch-exit-analysis.ts +100 -59
- package/src/tools/blocks/analysis.ts +116 -101
- package/src/tools/blocks/comparison.ts +281 -364
- package/src/tools/blocks/core.ts +208 -253
- package/src/tools/blocks/health.ts +571 -563
- package/src/tools/blocks/index.ts +2 -0
- package/src/tools/blocks/paired-comparison.ts +425 -0
- package/src/tools/blocks/similarity.ts +316 -283
- package/src/tools/edge-decay.ts +124 -197
- package/src/tools/exit-analysis.ts +130 -77
- package/src/tools/greeks-attribution.ts +84 -35
- package/src/tools/guides.ts +4 -6
- package/src/tools/imports.ts +11 -14
- package/src/tools/market-data.ts +1067 -757
- package/src/tools/market-enrichment.ts +3 -3
- package/src/tools/market-fetch.ts +148 -67
- package/src/tools/market-imports.ts +12 -12
- package/src/tools/middleware/sync-middleware.ts +5 -6
- package/src/tools/performance.ts +185 -302
- package/src/tools/profile-analysis.ts +52 -66
- package/src/tools/profiles.ts +106 -69
- package/src/tools/regime-advisor.ts +20 -45
- package/src/tools/replay.ts +81 -77
- package/src/tools/reports/discrepancies.ts +298 -328
- package/src/tools/reports/fields.ts +7 -25
- package/src/tools/reports/helpers.ts +18 -49
- package/src/tools/reports/predictive.ts +27 -70
- package/src/tools/reports/slippage-trends.ts +315 -345
- package/src/tools/reports/slippage.ts +1 -4
- package/src/tools/reports/strategy-matches.ts +399 -441
- package/src/tools/schema.ts +43 -40
- package/src/tools/shared/filters.ts +3 -9
- package/src/tools/snapshot.ts +9 -30
- package/src/tools/sql.ts +15 -14
- package/src/tools/tickers.ts +1 -4
- package/src/utils/batch-exit-analysis.ts +31 -42
- package/src/utils/black-scholes.ts +39 -29
- package/src/utils/block-loader.ts +69 -83
- package/src/utils/calibration-probe.ts +3 -4
- package/src/utils/chain-loader.ts +3 -3
- package/src/utils/csv-discovery.ts +16 -22
- package/src/utils/data-quality.ts +24 -36
- package/src/utils/exit-triggers.ts +91 -96
- package/src/utils/field-timing.ts +94 -79
- package/src/utils/filter-predicates.ts +13 -9
- package/src/utils/flatfile-importer.ts +94 -64
- package/src/utils/greeks-decomposition.ts +152 -100
- package/src/utils/iv-solver-pool.ts +55 -25
- package/src/utils/iv-solver-worker.ts +5 -5
- package/src/utils/market-enricher.ts +528 -497
- package/src/utils/market-importer.ts +31 -12
- package/src/utils/market-provider.ts +21 -23
- package/src/utils/massive-tier.ts +5 -7
- package/src/utils/migrate-option-data-helpers.ts +2 -8
- package/src/utils/option-quote-greeks.ts +25 -31
- package/src/utils/option-time.ts +4 -8
- package/src/utils/output-formatter.ts +1 -4
- package/src/utils/provider-capabilities.ts +1 -4
- package/src/utils/providers/massive.ts +59 -93
- package/src/utils/providers/thetadata/backfill.ts +14 -23
- package/src/utils/providers/thetadata/client.ts +12 -8
- package/src/utils/providers/thetadata/decode.ts +2 -20
- package/src/utils/providers/thetadata/endpoints.ts +32 -40
- package/src/utils/providers/thetadata/join.ts +11 -10
- package/src/utils/providers/thetadata/proto.ts +12 -10
- package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
- package/src/utils/providers/thetadata.ts +11 -10
- package/src/utils/quote-enricher.ts +4 -4
- package/src/utils/quote-parquet-projection.ts +3 -11
- package/src/utils/sample-date-selector.ts +3 -5
- package/src/utils/schema-metadata.ts +102 -70
- package/src/utils/ticker.ts +5 -9
- package/src/utils/trade-replay.ts +77 -68
- package/dist/chunk-4BLCXNQ6.js.map +0 -1
- package/dist/chunk-BKQ4PM4Y.js.map +0 -1
- package/dist/chunk-FGZH632F.js.map +0 -1
- package/dist/chunk-LDKTV7GW.js.map +0 -1
- package/dist/chunk-PRAYH3RT.js.map +0 -1
- package/dist/chunk-QTTR7AAW.js.map +0 -1
- package/dist/chunk-W2PP3LEH.js.map +0 -1
- package/dist/chunk-XXYOUIZY.js.map +0 -1
- package/dist/daily-log-processor-6MWJ23JK.js +0 -9
- package/dist/market-provider-VDRJUEF2.js +0 -16
- package/dist/trade-processor-NHU4VWRX.js +0 -9
- package/manifest.json +0 -141
- package/server/chunk-5EBXHT6C.js.map +0 -1
- package/server/chunk-72GKJE2U.js.map +0 -1
- package/server/chunk-CSDVJPBB.js.map +0 -1
- package/server/chunk-FBNDMCT5.js.map +0 -1
- package/server/chunk-NRFXAJF7.js.map +0 -1
- package/server/chunk-NZO6PT64.js.map +0 -1
- package/server/chunk-PNKG7RY7.js.map +0 -1
- package/server/chunk-WA5AAPCH.js.map +0 -1
- package/server/config-6IZXEFEX.js.map +0 -1
- package/server/daily-log-processor-Y3PVSVBM.js +0 -10
- package/server/market-provider-VOYYVYWT.js +0 -17
- package/server/trade-processor-JWVS37KM.js +0 -10
- /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-MZW2XBZY.js.map} +0 -0
- /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
- /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-6U33TQUT.js.map} +0 -0
- /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
- /package/dist/{sync-PO4IPCYV.js.map → sync-3UBC37VW.js.map} +0 -0
- /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-DKDCWGKD.js.map} +0 -0
- /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
- /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-B2VPU2FQ.js.map} +0 -0
- /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
- /package/server/{market-provider-VOYYVYWT.js.map → market-provider-L56RFHAC.js.map} +0 -0
- /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
- /package/server/{sync-QFI5L7S7.js.map → sync-CENKDN53.js.map} +0 -0
- /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-3PI4LHZI.js.map} +0 -0
- /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
package/src/tools/market-data.ts
CHANGED
|
@@ -15,10 +15,7 @@
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
17
|
import { loadBlock } from "../utils/block-loader.ts";
|
|
18
|
-
import {
|
|
19
|
-
createToolOutput,
|
|
20
|
-
formatPercent,
|
|
21
|
-
} from "../utils/output-formatter.ts";
|
|
18
|
+
import { createToolOutput, formatPercent } from "../utils/output-formatter.ts";
|
|
22
19
|
import type { Trade } from "@tradeblocks/lib";
|
|
23
20
|
import { getConnection } from "../db/connection.ts";
|
|
24
21
|
import { withFullSync } from "./middleware/sync-middleware.ts";
|
|
@@ -147,7 +144,11 @@ function formatTradeDate(date: Date | string): string {
|
|
|
147
144
|
* Convert DuckDB query result to an array of Record objects.
|
|
148
145
|
* Handles BigInt to Number conversion for JSON compatibility.
|
|
149
146
|
*/
|
|
150
|
-
function resultToRecords(result: {
|
|
147
|
+
function resultToRecords(result: {
|
|
148
|
+
columnCount: number;
|
|
149
|
+
columnName(i: number): string;
|
|
150
|
+
getRows(): Iterable<unknown[]>;
|
|
151
|
+
}): Record<string, unknown>[] {
|
|
151
152
|
const columnCount = result.columnCount;
|
|
152
153
|
const colNames: string[] = [];
|
|
153
154
|
for (let i = 0; i < columnCount; i++) {
|
|
@@ -193,7 +194,7 @@ function uniqueTradeLookupKeys(trades: Trade[]): MarketLookupKey[] {
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
function recordsByTickerDate(
|
|
196
|
-
records: Record<string, unknown>[]
|
|
197
|
+
records: Record<string, unknown>[],
|
|
197
198
|
): Map<string, Record<string, unknown>> {
|
|
198
199
|
const mapped = new Map<string, Record<string, unknown>>();
|
|
199
200
|
for (const record of records) {
|
|
@@ -275,12 +276,9 @@ export function registerMarketDataTools(
|
|
|
275
276
|
"Returns warnings when market data is partially missing.",
|
|
276
277
|
inputSchema: z.object({
|
|
277
278
|
blockId: z.string().describe("Block ID to analyze"),
|
|
278
|
-
segmentBy: z
|
|
279
|
-
"volRegime",
|
|
280
|
-
"
|
|
281
|
-
"dayOfWeek",
|
|
282
|
-
"gapDirection",
|
|
283
|
-
]).describe("Market dimension to segment by"),
|
|
279
|
+
segmentBy: z
|
|
280
|
+
.enum(["volRegime", "termStructure", "dayOfWeek", "gapDirection"])
|
|
281
|
+
.describe("Market dimension to segment by"),
|
|
284
282
|
strategy: z.string().optional().describe("Filter to specific strategy"),
|
|
285
283
|
ticker: z.string().optional().describe("Underlying ticker symbol (default: SPX)"),
|
|
286
284
|
}),
|
|
@@ -291,9 +289,7 @@ export function registerMarketDataTools(
|
|
|
291
289
|
let trades = block.trades;
|
|
292
290
|
|
|
293
291
|
if (strategy) {
|
|
294
|
-
trades = trades.filter(
|
|
295
|
-
(t) => t.strategy.toLowerCase() === strategy.toLowerCase()
|
|
296
|
-
);
|
|
292
|
+
trades = trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
|
|
297
293
|
}
|
|
298
294
|
|
|
299
295
|
if (trades.length === 0) {
|
|
@@ -309,7 +305,7 @@ export function registerMarketDataTools(
|
|
|
309
305
|
const conn = await getConnection(baseDir);
|
|
310
306
|
|
|
311
307
|
// Check data availability and collect warnings
|
|
312
|
-
const resolvedTicker = normalizeTicker(ticker ||
|
|
308
|
+
const resolvedTicker = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
|
|
313
309
|
const availability = await checkDataAvailability(stores, resolvedTicker);
|
|
314
310
|
|
|
315
311
|
const { sql: lagSql, params: lagParams } = buildLookaheadFreeQuery(tradeKeys);
|
|
@@ -350,7 +346,10 @@ export function registerMarketDataTools(
|
|
|
350
346
|
switch (segmentBy) {
|
|
351
347
|
case "volRegime": {
|
|
352
348
|
const val = getNum(marketData, "prev_Vol_Regime");
|
|
353
|
-
if (isNaN(val)) {
|
|
349
|
+
if (isNaN(val)) {
|
|
350
|
+
lagExcluded++;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
354
353
|
segmentValue = val;
|
|
355
354
|
segmentKey = String(val);
|
|
356
355
|
segmentLabel = getVolRegimeLabel(val);
|
|
@@ -358,7 +357,10 @@ export function registerMarketDataTools(
|
|
|
358
357
|
}
|
|
359
358
|
case "termStructure": {
|
|
360
359
|
const val = getNum(marketData, "prev_Term_Structure_State");
|
|
361
|
-
if (isNaN(val)) {
|
|
360
|
+
if (isNaN(val)) {
|
|
361
|
+
lagExcluded++;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
362
364
|
segmentValue = val;
|
|
363
365
|
segmentKey = String(val);
|
|
364
366
|
segmentLabel = getTermStructureLabel(val);
|
|
@@ -371,7 +373,10 @@ export function registerMarketDataTools(
|
|
|
371
373
|
break;
|
|
372
374
|
case "gapDirection": {
|
|
373
375
|
const gapPct = getNum(marketData, "Gap_Pct");
|
|
374
|
-
if (isNaN(gapPct)) {
|
|
376
|
+
if (isNaN(gapPct)) {
|
|
377
|
+
lagExcluded++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
375
380
|
segmentValue = gapPct > 0.1 ? "up" : gapPct < -0.1 ? "down" : "flat";
|
|
376
381
|
segmentKey = segmentValue;
|
|
377
382
|
segmentLabel = `Gap ${segmentValue}`;
|
|
@@ -419,16 +424,19 @@ export function registerMarketDataTools(
|
|
|
419
424
|
|
|
420
425
|
const winningTrades = seg.trades.filter((t) => t.isWin);
|
|
421
426
|
const losingTrades = seg.trades.filter((t) => !t.isWin);
|
|
422
|
-
const avgWin =
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
427
|
+
const avgWin =
|
|
428
|
+
winningTrades.length > 0
|
|
429
|
+
? winningTrades.reduce((sum, t) => sum + t.pl, 0) / winningTrades.length
|
|
430
|
+
: 0;
|
|
431
|
+
const avgLoss =
|
|
432
|
+
losingTrades.length > 0
|
|
433
|
+
? losingTrades.reduce((sum, t) => sum + t.pl, 0) / losingTrades.length
|
|
434
|
+
: 0;
|
|
428
435
|
|
|
429
436
|
const grossWins = winningTrades.reduce((sum, t) => sum + t.pl, 0);
|
|
430
437
|
const grossLosses = Math.abs(losingTrades.reduce((sum, t) => sum + t.pl, 0));
|
|
431
|
-
const profitFactor =
|
|
438
|
+
const profitFactor =
|
|
439
|
+
grossLosses > 0 ? grossWins / grossLosses : grossWins > 0 ? null : 0;
|
|
432
440
|
|
|
433
441
|
return {
|
|
434
442
|
segment: seg.segment,
|
|
@@ -494,7 +502,7 @@ export function registerMarketDataTools(
|
|
|
494
502
|
isError: true,
|
|
495
503
|
};
|
|
496
504
|
}
|
|
497
|
-
})
|
|
505
|
+
}),
|
|
498
506
|
);
|
|
499
507
|
|
|
500
508
|
// ---------------------------------------------------------------------------
|
|
@@ -512,371 +520,606 @@ export function registerMarketDataTools(
|
|
|
512
520
|
inputSchema: z.object({
|
|
513
521
|
blockId: z.string().describe("Block ID to analyze"),
|
|
514
522
|
strategy: z.string().optional().describe("Filter to specific strategy"),
|
|
515
|
-
strategyName: z
|
|
516
|
-
|
|
523
|
+
strategyName: z
|
|
524
|
+
.string()
|
|
525
|
+
.optional()
|
|
526
|
+
.describe(
|
|
527
|
+
"Strategy profile name. When provided, auto-filters to that strategy's trades and cross-references suggestions against profile's entry_filters.",
|
|
528
|
+
),
|
|
529
|
+
minImprovementPct: z
|
|
530
|
+
.number()
|
|
531
|
+
.optional()
|
|
532
|
+
.describe("Only suggest filters with >= X% win rate improvement (default: 3)"),
|
|
517
533
|
ticker: z.string().optional().describe("Underlying ticker symbol (default: SPX)"),
|
|
518
534
|
}),
|
|
519
535
|
},
|
|
520
|
-
withFullSync(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
536
|
+
withFullSync(
|
|
537
|
+
baseDir,
|
|
538
|
+
async ({ blockId, strategy, strategyName, minImprovementPct = 3, ticker }) => {
|
|
539
|
+
try {
|
|
540
|
+
const block = await loadBlock(baseDir, blockId);
|
|
541
|
+
let trades = block.trades;
|
|
542
|
+
|
|
543
|
+
// If strategyName provided, use it to filter trades (takes precedence over strategy)
|
|
544
|
+
const effectiveStrategy = strategyName || strategy;
|
|
545
|
+
if (effectiveStrategy) {
|
|
546
|
+
trades = filterByStrategy(trades, effectiveStrategy);
|
|
547
|
+
// Single-strategy fallback: profile strategyName may differ from CSV strategy label
|
|
548
|
+
if (trades.length === 0 && block.trades.length > 0) {
|
|
549
|
+
const uniqueStrategies = new Set(block.trades.map((t) => t.strategy));
|
|
550
|
+
if (uniqueStrategies.size === 1) {
|
|
551
|
+
trades = block.trades;
|
|
552
|
+
}
|
|
534
553
|
}
|
|
535
554
|
}
|
|
536
|
-
}
|
|
537
555
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
556
|
+
// Load profile for cross-referencing if strategyName provided
|
|
557
|
+
let profileEntryFilters: Array<{
|
|
558
|
+
field: string;
|
|
559
|
+
operator: string;
|
|
560
|
+
value: unknown;
|
|
561
|
+
description?: string;
|
|
562
|
+
}> | null = null;
|
|
563
|
+
if (strategyName) {
|
|
564
|
+
const conn = await getConnection(baseDir);
|
|
565
|
+
const profile = await getProfile(conn, blockId, strategyName, baseDir);
|
|
566
|
+
if (profile) {
|
|
567
|
+
profileEntryFilters = profile.entryFilters;
|
|
568
|
+
}
|
|
545
569
|
}
|
|
546
|
-
}
|
|
547
570
|
|
|
548
|
-
|
|
571
|
+
// Note: strategy filtering is now handled above via effectiveStrategy
|
|
549
572
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
573
|
+
if (trades.length === 0) {
|
|
574
|
+
return {
|
|
575
|
+
content: [{ type: "text", text: "No trades found" }],
|
|
576
|
+
isError: true,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
556
579
|
|
|
557
|
-
|
|
558
|
-
|
|
580
|
+
// Collect unique trade keys (ticker+date) and query DuckDB for market data
|
|
581
|
+
const tradeKeys = uniqueTradeLookupKeys(trades);
|
|
559
582
|
|
|
560
|
-
|
|
583
|
+
const conn = await getConnection(baseDir);
|
|
561
584
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
585
|
+
// Check data availability and collect warnings
|
|
586
|
+
const resolvedTickerSF = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
|
|
587
|
+
const availabilitySF = await checkDataAvailability(stores, resolvedTickerSF);
|
|
565
588
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
589
|
+
const { sql, params } = buildLookaheadFreeQuery(tradeKeys);
|
|
590
|
+
const dailyResult = await conn.runAndReadAll(sql, params);
|
|
591
|
+
const dailyRecords = resultToRecords(dailyResult);
|
|
592
|
+
const daily = recordsByTickerDate(dailyRecords);
|
|
570
593
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
594
|
+
// Match trades to market data
|
|
595
|
+
interface EnrichedTrade {
|
|
596
|
+
trade: Trade;
|
|
597
|
+
market: Record<string, unknown> | null;
|
|
598
|
+
}
|
|
576
599
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
};
|
|
584
|
-
});
|
|
600
|
+
const enrichedTrades: EnrichedTrade[] = trades.map((trade) => {
|
|
601
|
+
const lookup = getTradeLookupKey(trade);
|
|
602
|
+
return {
|
|
603
|
+
trade,
|
|
604
|
+
market: daily.get(marketTickerDateKey(lookup.ticker, lookup.date)) || null,
|
|
605
|
+
};
|
|
606
|
+
});
|
|
585
607
|
|
|
586
|
-
|
|
608
|
+
const matchedTrades = enrichedTrades.filter((t) => t.market !== null);
|
|
587
609
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
610
|
+
if (matchedTrades.length < 10) {
|
|
611
|
+
return {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: "text",
|
|
615
|
+
text: "Not enough trades matched to market data for analysis (need at least 10)",
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
isError: true,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
594
621
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
622
|
+
// Calculate current stats
|
|
623
|
+
const currentWins = matchedTrades.filter((t) => t.trade.pl > 0).length;
|
|
624
|
+
const currentWinRate = (currentWins / matchedTrades.length) * 100;
|
|
625
|
+
const currentTotalPl = matchedTrades.reduce((sum, t) => sum + t.trade.pl, 0);
|
|
626
|
+
|
|
627
|
+
// Test various filters
|
|
628
|
+
interface FilterSuggestion {
|
|
629
|
+
filter: string;
|
|
630
|
+
condition: {
|
|
631
|
+
field: string;
|
|
632
|
+
operator: string;
|
|
633
|
+
value: number | number[] | string;
|
|
634
|
+
lagged: boolean;
|
|
635
|
+
};
|
|
636
|
+
tradesRemoved: number;
|
|
637
|
+
winnersRemoved: number;
|
|
638
|
+
losersRemoved: number;
|
|
639
|
+
newWinRate: number;
|
|
640
|
+
newTotalPl: number;
|
|
641
|
+
winRateDelta: number;
|
|
642
|
+
plDelta: number;
|
|
643
|
+
confidence: "high" | "medium" | "low";
|
|
644
|
+
}
|
|
599
645
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
filter
|
|
603
|
-
|
|
646
|
+
const suggestions: FilterSuggestion[] = [];
|
|
647
|
+
|
|
648
|
+
// Test filter functions
|
|
649
|
+
const testFilters: Array<{
|
|
650
|
+
name: string;
|
|
604
651
|
field: string;
|
|
605
652
|
operator: string;
|
|
606
653
|
value: number | number[] | string;
|
|
654
|
+
test: (m: Record<string, unknown>) => boolean;
|
|
607
655
|
lagged: boolean;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
656
|
+
}> = [
|
|
657
|
+
// Open-known filters (same-day values)
|
|
658
|
+
// Gap filters
|
|
659
|
+
{
|
|
660
|
+
name: "Skip when |Gap_Pct| > 0.5%",
|
|
661
|
+
field: "Gap_Pct",
|
|
662
|
+
operator: ">",
|
|
663
|
+
value: 0.5,
|
|
664
|
+
test: (m) => Math.abs(getNum(m, "Gap_Pct")) > 0.5,
|
|
665
|
+
lagged: false,
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: "Skip when |Gap_Pct| > 0.8%",
|
|
669
|
+
field: "Gap_Pct",
|
|
670
|
+
operator: ">",
|
|
671
|
+
value: 0.8,
|
|
672
|
+
test: (m) => Math.abs(getNum(m, "Gap_Pct")) > 0.8,
|
|
673
|
+
lagged: false,
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: "Skip when |Gap_Pct| > 1.0%",
|
|
677
|
+
field: "Gap_Pct",
|
|
678
|
+
operator: ">",
|
|
679
|
+
value: 1.0,
|
|
680
|
+
test: (m) => Math.abs(getNum(m, "Gap_Pct")) > 1.0,
|
|
681
|
+
lagged: false,
|
|
682
|
+
},
|
|
683
|
+
// Day of week
|
|
684
|
+
{
|
|
685
|
+
name: "Skip Fridays",
|
|
686
|
+
field: "Day_of_Week",
|
|
687
|
+
operator: "==",
|
|
688
|
+
value: 6,
|
|
689
|
+
test: (m) => getNum(m, "Day_of_Week") === 6,
|
|
690
|
+
lagged: false,
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "Skip Mondays",
|
|
694
|
+
field: "Day_of_Week",
|
|
695
|
+
operator: "==",
|
|
696
|
+
value: 2,
|
|
697
|
+
test: (m) => getNum(m, "Day_of_Week") === 2,
|
|
698
|
+
lagged: false,
|
|
699
|
+
},
|
|
700
|
+
// OPEX
|
|
701
|
+
{
|
|
702
|
+
name: "Skip OPEX days",
|
|
703
|
+
field: "Is_Opex",
|
|
704
|
+
operator: "==",
|
|
705
|
+
value: 1,
|
|
706
|
+
test: (m) => getNum(m, "Is_Opex") === 1,
|
|
707
|
+
lagged: false,
|
|
708
|
+
},
|
|
709
|
+
// VIX_Open filters (open-known)
|
|
710
|
+
{
|
|
711
|
+
name: "Skip when VIX_Open > 25",
|
|
712
|
+
field: "VIX_Open",
|
|
713
|
+
operator: ">",
|
|
714
|
+
value: 25,
|
|
715
|
+
test: (m) => getNum(m, "VIX_Open") > 25,
|
|
716
|
+
lagged: false,
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: "Skip when VIX_Open > 30",
|
|
720
|
+
field: "VIX_Open",
|
|
721
|
+
operator: ">",
|
|
722
|
+
value: 30,
|
|
723
|
+
test: (m) => getNum(m, "VIX_Open") > 30,
|
|
724
|
+
lagged: false,
|
|
725
|
+
},
|
|
726
|
+
// VIX_Gap_Pct filters (open-known)
|
|
727
|
+
{
|
|
728
|
+
name: "Skip when |VIX_Gap_Pct| > 10%",
|
|
729
|
+
field: "VIX_Gap_Pct",
|
|
730
|
+
operator: ">",
|
|
731
|
+
value: 10,
|
|
732
|
+
test: (m) => Math.abs(getNum(m, "VIX_Gap_Pct")) > 10,
|
|
733
|
+
lagged: false,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "Skip when |VIX_Gap_Pct| > 15%",
|
|
737
|
+
field: "VIX_Gap_Pct",
|
|
738
|
+
operator: ">",
|
|
739
|
+
value: 15,
|
|
740
|
+
test: (m) => Math.abs(getNum(m, "VIX_Gap_Pct")) > 15,
|
|
741
|
+
lagged: false,
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
// Close-derived filters (prior trading day values via LAG CTE)
|
|
745
|
+
// VIX (close-derived)
|
|
746
|
+
{
|
|
747
|
+
name: "Skip when prior-day VIX > 25",
|
|
748
|
+
field: "VIX_Close",
|
|
749
|
+
operator: ">",
|
|
750
|
+
value: 25,
|
|
751
|
+
test: (m) => getNum(m, "prev_VIX_Close") > 25,
|
|
752
|
+
lagged: true,
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: "Skip when prior-day VIX > 30",
|
|
756
|
+
field: "VIX_Close",
|
|
757
|
+
operator: ">",
|
|
758
|
+
value: 30,
|
|
759
|
+
test: (m) => getNum(m, "prev_VIX_Close") > 30,
|
|
760
|
+
lagged: true,
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: "Skip when prior-day VIX < 14",
|
|
764
|
+
field: "VIX_Close",
|
|
765
|
+
operator: "<",
|
|
766
|
+
value: 14,
|
|
767
|
+
test: (m) => getNum(m, "prev_VIX_Close") < 14,
|
|
768
|
+
lagged: true,
|
|
769
|
+
},
|
|
770
|
+
// VIX spike (close-derived)
|
|
771
|
+
{
|
|
772
|
+
name: "Skip when prior-day VIX_Spike > 5%",
|
|
773
|
+
field: "VIX_Spike_Pct",
|
|
774
|
+
operator: ">",
|
|
775
|
+
value: 5,
|
|
776
|
+
test: (m) => getNum(m, "prev_VIX_Spike_Pct") > 5,
|
|
777
|
+
lagged: true,
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
name: "Skip when prior-day VIX_Spike > 8%",
|
|
781
|
+
field: "VIX_Spike_Pct",
|
|
782
|
+
operator: ">",
|
|
783
|
+
value: 8,
|
|
784
|
+
test: (m) => getNum(m, "prev_VIX_Spike_Pct") > 8,
|
|
785
|
+
lagged: true,
|
|
786
|
+
},
|
|
787
|
+
// Term structure (close-derived)
|
|
788
|
+
{
|
|
789
|
+
name: "Skip prior-day backwardation",
|
|
790
|
+
field: "Term_Structure_State",
|
|
791
|
+
operator: "==",
|
|
792
|
+
value: -1,
|
|
793
|
+
test: (m) => getNum(m, "prev_Term_Structure_State") === -1,
|
|
794
|
+
lagged: true,
|
|
795
|
+
},
|
|
796
|
+
// Vol regime (close-derived)
|
|
797
|
+
{
|
|
798
|
+
name: "Skip prior-day Vol Regime 5-6 (High/Extreme)",
|
|
799
|
+
field: "Vol_Regime",
|
|
800
|
+
operator: "in",
|
|
801
|
+
value: [5, 6],
|
|
802
|
+
test: (m) => getNum(m, "prev_Vol_Regime") >= 5,
|
|
803
|
+
lagged: true,
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
name: "Skip prior-day Vol Regime 1 (Very Low)",
|
|
807
|
+
field: "Vol_Regime",
|
|
808
|
+
operator: "==",
|
|
809
|
+
value: 1,
|
|
810
|
+
test: (m) => getNum(m, "prev_Vol_Regime") === 1,
|
|
811
|
+
lagged: true,
|
|
812
|
+
},
|
|
813
|
+
// Consecutive days (close-derived)
|
|
814
|
+
{
|
|
815
|
+
name: "Skip after prior-day 4+ consecutive up",
|
|
816
|
+
field: "Consecutive_Days",
|
|
817
|
+
operator: ">=",
|
|
818
|
+
value: 4,
|
|
819
|
+
test: (m) => getNum(m, "prev_Consecutive_Days") >= 4,
|
|
820
|
+
lagged: true,
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
name: "Skip after prior-day 4+ consecutive down",
|
|
824
|
+
field: "Consecutive_Days",
|
|
825
|
+
operator: "<=",
|
|
826
|
+
value: -4,
|
|
827
|
+
test: (m) => getNum(m, "prev_Consecutive_Days") <= -4,
|
|
828
|
+
lagged: true,
|
|
829
|
+
},
|
|
830
|
+
// RSI (close-derived)
|
|
831
|
+
{
|
|
832
|
+
name: "Skip when prior-day RSI > 70",
|
|
833
|
+
field: "RSI_14",
|
|
834
|
+
operator: ">",
|
|
835
|
+
value: 70,
|
|
836
|
+
test: (m) => getNum(m, "prev_RSI_14") > 70,
|
|
837
|
+
lagged: true,
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: "Skip when prior-day RSI < 30",
|
|
841
|
+
field: "RSI_14",
|
|
842
|
+
operator: "<",
|
|
843
|
+
value: 30,
|
|
844
|
+
test: (m) => getNum(m, "prev_RSI_14") < 30,
|
|
845
|
+
lagged: true,
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
// Realized Vol filters (close-derived, from market.enriched)
|
|
849
|
+
{
|
|
850
|
+
name: "Skip when prior-day 5D realized vol > 1.5%",
|
|
851
|
+
field: "Realized_Vol_5D",
|
|
852
|
+
operator: ">",
|
|
853
|
+
value: 1.5,
|
|
854
|
+
test: (m) => getNum(m, "prev_Realized_Vol_5D") > 1.5,
|
|
855
|
+
lagged: true,
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
name: "Skip when prior-day 20D realized vol > 1.2%",
|
|
859
|
+
field: "Realized_Vol_20D",
|
|
860
|
+
operator: ">",
|
|
861
|
+
value: 1.2,
|
|
862
|
+
test: (m) => getNum(m, "prev_Realized_Vol_20D") > 1.2,
|
|
863
|
+
lagged: true,
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
// IVP filters (close-derived, from market.enriched ivr/ivp columns)
|
|
867
|
+
{
|
|
868
|
+
name: "Skip when prior-day VIX_IVP > 80 (top 20% historically elevated vol)",
|
|
869
|
+
field: "VIX_IVP",
|
|
870
|
+
operator: ">",
|
|
871
|
+
value: 80,
|
|
872
|
+
test: (m) => getNum(m, "prev_VIX_IVP") > 80,
|
|
873
|
+
lagged: true,
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
name: "Skip when prior-day VIX_IVP < 20 (bottom 20% historically suppressed vol)",
|
|
877
|
+
field: "VIX_IVP",
|
|
878
|
+
operator: "<",
|
|
879
|
+
value: 20,
|
|
880
|
+
test: (m) => getNum(m, "prev_VIX_IVP") < 20,
|
|
881
|
+
lagged: true,
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
// Prior_Range_vs_ATR filter (open-known, from market.enriched — same-day value)
|
|
885
|
+
{
|
|
886
|
+
name: "Skip when Prior_Range_vs_ATR > 1.5 (prior day had outsized range)",
|
|
887
|
+
field: "Prior_Range_vs_ATR",
|
|
888
|
+
operator: ">",
|
|
889
|
+
value: 1.5,
|
|
890
|
+
test: (m) => getNum(m, "Prior_Range_vs_ATR") > 1.5,
|
|
891
|
+
lagged: false,
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: "Skip when Prior_Range_vs_ATR < 0.5 (prior day had compressed range)",
|
|
895
|
+
field: "Prior_Range_vs_ATR",
|
|
896
|
+
operator: "<",
|
|
897
|
+
value: 0.5,
|
|
898
|
+
test: (m) => getNum(m, "Prior_Range_vs_ATR") < 0.5,
|
|
899
|
+
lagged: false,
|
|
900
|
+
},
|
|
901
|
+
];
|
|
902
|
+
|
|
903
|
+
for (const filterDef of testFilters) {
|
|
904
|
+
// For lagged filters, exclude trades with NaN lag values from evaluation
|
|
905
|
+
// (prevents NaN comparisons from silently passing all tests and biasing results)
|
|
906
|
+
let pool = matchedTrades;
|
|
907
|
+
if (filterDef.lagged) {
|
|
908
|
+
const prevField = `prev_${filterDef.field}`;
|
|
909
|
+
pool = matchedTrades.filter((t) => {
|
|
910
|
+
const val = getNum(t.market as Record<string, unknown>, prevField);
|
|
911
|
+
return !isNaN(val);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
618
914
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// Test filter functions
|
|
622
|
-
const testFilters: Array<{
|
|
623
|
-
name: string;
|
|
624
|
-
field: string;
|
|
625
|
-
operator: string;
|
|
626
|
-
value: number | number[] | string;
|
|
627
|
-
test: (m: Record<string, unknown>) => boolean;
|
|
628
|
-
lagged: boolean;
|
|
629
|
-
}> = [
|
|
630
|
-
// Open-known filters (same-day values)
|
|
631
|
-
// Gap filters
|
|
632
|
-
{ name: "Skip when |Gap_Pct| > 0.5%", field: "Gap_Pct", operator: ">", value: 0.5, test: (m) => Math.abs(getNum(m, "Gap_Pct")) > 0.5, lagged: false },
|
|
633
|
-
{ name: "Skip when |Gap_Pct| > 0.8%", field: "Gap_Pct", operator: ">", value: 0.8, test: (m) => Math.abs(getNum(m, "Gap_Pct")) > 0.8, lagged: false },
|
|
634
|
-
{ name: "Skip when |Gap_Pct| > 1.0%", field: "Gap_Pct", operator: ">", value: 1.0, test: (m) => Math.abs(getNum(m, "Gap_Pct")) > 1.0, lagged: false },
|
|
635
|
-
// Day of week
|
|
636
|
-
{ name: "Skip Fridays", field: "Day_of_Week", operator: "==", value: 6, test: (m) => getNum(m, "Day_of_Week") === 6, lagged: false },
|
|
637
|
-
{ name: "Skip Mondays", field: "Day_of_Week", operator: "==", value: 2, test: (m) => getNum(m, "Day_of_Week") === 2, lagged: false },
|
|
638
|
-
// OPEX
|
|
639
|
-
{ name: "Skip OPEX days", field: "Is_Opex", operator: "==", value: 1, test: (m) => getNum(m, "Is_Opex") === 1, lagged: false },
|
|
640
|
-
// VIX_Open filters (open-known)
|
|
641
|
-
{ name: "Skip when VIX_Open > 25", field: "VIX_Open", operator: ">", value: 25, test: (m) => getNum(m, "VIX_Open") > 25, lagged: false },
|
|
642
|
-
{ name: "Skip when VIX_Open > 30", field: "VIX_Open", operator: ">", value: 30, test: (m) => getNum(m, "VIX_Open") > 30, lagged: false },
|
|
643
|
-
// VIX_Gap_Pct filters (open-known)
|
|
644
|
-
{ name: "Skip when |VIX_Gap_Pct| > 10%", field: "VIX_Gap_Pct", operator: ">", value: 10, test: (m) => Math.abs(getNum(m, "VIX_Gap_Pct")) > 10, lagged: false },
|
|
645
|
-
{ name: "Skip when |VIX_Gap_Pct| > 15%", field: "VIX_Gap_Pct", operator: ">", value: 15, test: (m) => Math.abs(getNum(m, "VIX_Gap_Pct")) > 15, lagged: false },
|
|
646
|
-
|
|
647
|
-
// Close-derived filters (prior trading day values via LAG CTE)
|
|
648
|
-
// VIX (close-derived)
|
|
649
|
-
{ name: "Skip when prior-day VIX > 25", field: "VIX_Close", operator: ">", value: 25, test: (m) => getNum(m, "prev_VIX_Close") > 25, lagged: true },
|
|
650
|
-
{ name: "Skip when prior-day VIX > 30", field: "VIX_Close", operator: ">", value: 30, test: (m) => getNum(m, "prev_VIX_Close") > 30, lagged: true },
|
|
651
|
-
{ name: "Skip when prior-day VIX < 14", field: "VIX_Close", operator: "<", value: 14, test: (m) => getNum(m, "prev_VIX_Close") < 14, lagged: true },
|
|
652
|
-
// VIX spike (close-derived)
|
|
653
|
-
{ name: "Skip when prior-day VIX_Spike > 5%", field: "VIX_Spike_Pct", operator: ">", value: 5, test: (m) => getNum(m, "prev_VIX_Spike_Pct") > 5, lagged: true },
|
|
654
|
-
{ name: "Skip when prior-day VIX_Spike > 8%", field: "VIX_Spike_Pct", operator: ">", value: 8, test: (m) => getNum(m, "prev_VIX_Spike_Pct") > 8, lagged: true },
|
|
655
|
-
// Term structure (close-derived)
|
|
656
|
-
{ name: "Skip prior-day backwardation", field: "Term_Structure_State", operator: "==", value: -1, test: (m) => getNum(m, "prev_Term_Structure_State") === -1, lagged: true },
|
|
657
|
-
// Vol regime (close-derived)
|
|
658
|
-
{ name: "Skip prior-day Vol Regime 5-6 (High/Extreme)", field: "Vol_Regime", operator: "in", value: [5, 6], test: (m) => getNum(m, "prev_Vol_Regime") >= 5, lagged: true },
|
|
659
|
-
{ name: "Skip prior-day Vol Regime 1 (Very Low)", field: "Vol_Regime", operator: "==", value: 1, test: (m) => getNum(m, "prev_Vol_Regime") === 1, lagged: true },
|
|
660
|
-
// Consecutive days (close-derived)
|
|
661
|
-
{ name: "Skip after prior-day 4+ consecutive up", field: "Consecutive_Days", operator: ">=", value: 4, test: (m) => getNum(m, "prev_Consecutive_Days") >= 4, lagged: true },
|
|
662
|
-
{ name: "Skip after prior-day 4+ consecutive down", field: "Consecutive_Days", operator: "<=", value: -4, test: (m) => getNum(m, "prev_Consecutive_Days") <= -4, lagged: true },
|
|
663
|
-
// RSI (close-derived)
|
|
664
|
-
{ name: "Skip when prior-day RSI > 70", field: "RSI_14", operator: ">", value: 70, test: (m) => getNum(m, "prev_RSI_14") > 70, lagged: true },
|
|
665
|
-
{ name: "Skip when prior-day RSI < 30", field: "RSI_14", operator: "<", value: 30, test: (m) => getNum(m, "prev_RSI_14") < 30, lagged: true },
|
|
666
|
-
|
|
667
|
-
// Realized Vol filters (close-derived, from market.enriched)
|
|
668
|
-
{ name: "Skip when prior-day 5D realized vol > 1.5%", field: "Realized_Vol_5D", operator: ">", value: 1.5, test: (m) => getNum(m, "prev_Realized_Vol_5D") > 1.5, lagged: true },
|
|
669
|
-
{ name: "Skip when prior-day 20D realized vol > 1.2%", field: "Realized_Vol_20D", operator: ">", value: 1.2, test: (m) => getNum(m, "prev_Realized_Vol_20D") > 1.2, lagged: true },
|
|
670
|
-
|
|
671
|
-
// IVP filters (close-derived, from market.enriched ivr/ivp columns)
|
|
672
|
-
{ name: "Skip when prior-day VIX_IVP > 80 (top 20% historically elevated vol)", field: "VIX_IVP", operator: ">", value: 80, test: (m) => getNum(m, "prev_VIX_IVP") > 80, lagged: true },
|
|
673
|
-
{ name: "Skip when prior-day VIX_IVP < 20 (bottom 20% historically suppressed vol)", field: "VIX_IVP", operator: "<", value: 20, test: (m) => getNum(m, "prev_VIX_IVP") < 20, lagged: true },
|
|
674
|
-
|
|
675
|
-
// Prior_Range_vs_ATR filter (open-known, from market.enriched — same-day value)
|
|
676
|
-
{ name: "Skip when Prior_Range_vs_ATR > 1.5 (prior day had outsized range)", field: "Prior_Range_vs_ATR", operator: ">", value: 1.5, test: (m) => getNum(m, "Prior_Range_vs_ATR") > 1.5, lagged: false },
|
|
677
|
-
{ name: "Skip when Prior_Range_vs_ATR < 0.5 (prior day had compressed range)", field: "Prior_Range_vs_ATR", operator: "<", value: 0.5, test: (m) => getNum(m, "Prior_Range_vs_ATR") < 0.5, lagged: false },
|
|
678
|
-
];
|
|
679
|
-
|
|
680
|
-
for (const filterDef of testFilters) {
|
|
681
|
-
// For lagged filters, exclude trades with NaN lag values from evaluation
|
|
682
|
-
// (prevents NaN comparisons from silently passing all tests and biasing results)
|
|
683
|
-
let pool = matchedTrades;
|
|
684
|
-
if (filterDef.lagged) {
|
|
685
|
-
const prevField = `prev_${filterDef.field}`;
|
|
686
|
-
pool = matchedTrades.filter((t) => {
|
|
687
|
-
const val = getNum(t.market as Record<string, unknown>, prevField);
|
|
688
|
-
return !isNaN(val);
|
|
689
|
-
});
|
|
690
|
-
}
|
|
915
|
+
if (pool.length < 10) continue;
|
|
691
916
|
|
|
692
|
-
|
|
917
|
+
// Identify trades that would be removed
|
|
918
|
+
const removed = pool.filter((t) => filterDef.test(t.market as Record<string, unknown>));
|
|
919
|
+
const remaining = pool.filter(
|
|
920
|
+
(t) => !filterDef.test(t.market as Record<string, unknown>),
|
|
921
|
+
);
|
|
693
922
|
|
|
694
|
-
|
|
695
|
-
const removed = pool.filter((t) => filterDef.test(t.market as Record<string, unknown>));
|
|
696
|
-
const remaining = pool.filter((t) => !filterDef.test(t.market as Record<string, unknown>));
|
|
923
|
+
if (removed.length === 0 || remaining.length < 5) continue;
|
|
697
924
|
|
|
698
|
-
|
|
925
|
+
const poolWins = pool.filter((t) => t.trade.pl > 0).length;
|
|
926
|
+
const poolWinRate = (poolWins / pool.length) * 100;
|
|
927
|
+
const poolTotalPl = pool.reduce((sum, t) => sum + t.trade.pl, 0);
|
|
699
928
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const poolTotalPl = pool.reduce((sum, t) => sum + t.trade.pl, 0);
|
|
929
|
+
const winnersRemoved = removed.filter((t) => t.trade.pl > 0).length;
|
|
930
|
+
const losersRemoved = removed.length - winnersRemoved;
|
|
703
931
|
|
|
704
|
-
|
|
705
|
-
|
|
932
|
+
const newWins = remaining.filter((t) => t.trade.pl > 0).length;
|
|
933
|
+
const newWinRate = (newWins / remaining.length) * 100;
|
|
934
|
+
const newTotalPl = remaining.reduce((sum, t) => sum + t.trade.pl, 0);
|
|
706
935
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const newTotalPl = remaining.reduce((sum, t) => sum + t.trade.pl, 0);
|
|
936
|
+
const winRateDelta = newWinRate - poolWinRate;
|
|
937
|
+
const plDelta = newTotalPl - poolTotalPl;
|
|
710
938
|
|
|
711
|
-
|
|
712
|
-
|
|
939
|
+
// Only include if improvement meets threshold
|
|
940
|
+
if (winRateDelta >= minImprovementPct) {
|
|
941
|
+
// Determine confidence based on sample size
|
|
942
|
+
let confidence: "high" | "medium" | "low" = "low";
|
|
943
|
+
if (removed.length >= 10 && remaining.length >= 20) {
|
|
944
|
+
confidence = "high";
|
|
945
|
+
} else if (removed.length >= 5 && remaining.length >= 10) {
|
|
946
|
+
confidence = "medium";
|
|
947
|
+
}
|
|
713
948
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
949
|
+
suggestions.push({
|
|
950
|
+
filter: filterDef.name,
|
|
951
|
+
condition: {
|
|
952
|
+
field: filterDef.field,
|
|
953
|
+
operator: filterDef.operator,
|
|
954
|
+
value: filterDef.value,
|
|
955
|
+
lagged: filterDef.lagged,
|
|
956
|
+
},
|
|
957
|
+
tradesRemoved: removed.length,
|
|
958
|
+
winnersRemoved,
|
|
959
|
+
losersRemoved,
|
|
960
|
+
newWinRate: Math.round(newWinRate * 100) / 100,
|
|
961
|
+
newTotalPl: Math.round(newTotalPl * 100) / 100,
|
|
962
|
+
winRateDelta: Math.round(winRateDelta * 100) / 100,
|
|
963
|
+
plDelta: Math.round(plDelta * 100) / 100,
|
|
964
|
+
confidence,
|
|
965
|
+
});
|
|
722
966
|
}
|
|
723
|
-
|
|
724
|
-
suggestions.push({
|
|
725
|
-
filter: filterDef.name,
|
|
726
|
-
condition: {
|
|
727
|
-
field: filterDef.field,
|
|
728
|
-
operator: filterDef.operator,
|
|
729
|
-
value: filterDef.value,
|
|
730
|
-
lagged: filterDef.lagged,
|
|
731
|
-
},
|
|
732
|
-
tradesRemoved: removed.length,
|
|
733
|
-
winnersRemoved,
|
|
734
|
-
losersRemoved,
|
|
735
|
-
newWinRate: Math.round(newWinRate * 100) / 100,
|
|
736
|
-
newTotalPl: Math.round(newTotalPl * 100) / 100,
|
|
737
|
-
winRateDelta: Math.round(winRateDelta * 100) / 100,
|
|
738
|
-
plDelta: Math.round(plDelta * 100) / 100,
|
|
739
|
-
confidence,
|
|
740
|
-
});
|
|
741
967
|
}
|
|
742
|
-
}
|
|
743
968
|
|
|
744
|
-
|
|
745
|
-
|
|
969
|
+
// Sort by win rate improvement
|
|
970
|
+
suggestions.sort((a, b) => b.winRateDelta - a.winRateDelta);
|
|
746
971
|
|
|
747
|
-
|
|
748
|
-
|
|
972
|
+
// Take top 10
|
|
973
|
+
const topSuggestions = suggestions.slice(0, 10);
|
|
749
974
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
interface CompositeSuggestion {
|
|
755
|
-
name: string;
|
|
756
|
-
type: "composite";
|
|
757
|
-
projectedWinRate: number;
|
|
758
|
-
projectedAvgPl: number;
|
|
759
|
-
tradesAffected: number;
|
|
760
|
-
improvement: number;
|
|
761
|
-
}
|
|
975
|
+
// Generate composite filter suggestions from pairs of top-performing standalone filters
|
|
976
|
+
const baseWinRate = currentWinRate;
|
|
977
|
+
const significantFilters = suggestions.filter((s) => s.winRateDelta > 3);
|
|
762
978
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
for (let i = 0; i < significantFilters.length; i++) {
|
|
772
|
-
for (let j = i + 1; j < significantFilters.length; j++) {
|
|
773
|
-
const filterA = significantFilters[i];
|
|
774
|
-
const filterB = significantFilters[j];
|
|
979
|
+
interface CompositeSuggestion {
|
|
980
|
+
name: string;
|
|
981
|
+
type: "composite";
|
|
982
|
+
projectedWinRate: number;
|
|
983
|
+
projectedAvgPl: number;
|
|
984
|
+
tradesAffected: number;
|
|
985
|
+
improvement: number;
|
|
986
|
+
}
|
|
775
987
|
|
|
776
|
-
|
|
777
|
-
const testB = filterTestMap.get(filterB.filter);
|
|
778
|
-
if (!testA || !testB) continue;
|
|
988
|
+
const compositeSuggestions: CompositeSuggestion[] = [];
|
|
779
989
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
pool = pool.filter((t) => !isNaN(getNum(t.market as Record<string, unknown>, prevField)));
|
|
789
|
-
}
|
|
990
|
+
// Build a map from filter name to the original test function and lagged flag
|
|
991
|
+
const filterTestMap = new Map<
|
|
992
|
+
string,
|
|
993
|
+
{ test: (m: Record<string, unknown>) => boolean; lagged: boolean; field: string }
|
|
994
|
+
>();
|
|
995
|
+
for (const fd of testFilters) {
|
|
996
|
+
filterTestMap.set(fd.name, { test: fd.test, lagged: fd.lagged, field: fd.field });
|
|
997
|
+
}
|
|
790
998
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
999
|
+
for (let i = 0; i < significantFilters.length; i++) {
|
|
1000
|
+
for (let j = i + 1; j < significantFilters.length; j++) {
|
|
1001
|
+
const filterA = significantFilters[i];
|
|
1002
|
+
const filterB = significantFilters[j];
|
|
1003
|
+
|
|
1004
|
+
const testA = filterTestMap.get(filterA.filter);
|
|
1005
|
+
const testB = filterTestMap.get(filterB.filter);
|
|
1006
|
+
if (!testA || !testB) continue;
|
|
1007
|
+
|
|
1008
|
+
// Build pool: exclude NaN-lag trades for lagged fields in either filter
|
|
1009
|
+
let pool = matchedTrades;
|
|
1010
|
+
if (testA.lagged) {
|
|
1011
|
+
const prevField = `prev_${testA.field}`;
|
|
1012
|
+
pool = pool.filter(
|
|
1013
|
+
(t) => !isNaN(getNum(t.market as Record<string, unknown>, prevField)),
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (testB.lagged) {
|
|
1017
|
+
const prevField = `prev_${testB.field}`;
|
|
1018
|
+
pool = pool.filter(
|
|
1019
|
+
(t) => !isNaN(getNum(t.market as Record<string, unknown>, prevField)),
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
796
1022
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
// Only surface if composite win rate is materially better than either standalone filter alone
|
|
803
|
-
const improvement = compositeWinRate - Math.max(filterA.winRateDelta + baseWinRate, filterB.winRateDelta + baseWinRate);
|
|
804
|
-
if (improvement > 2) {
|
|
805
|
-
compositeSuggestions.push({
|
|
806
|
-
name: `${filterA.filter} AND ${filterB.filter}`,
|
|
807
|
-
type: "composite",
|
|
808
|
-
projectedWinRate: Math.round(compositeWinRate * 100) / 100,
|
|
809
|
-
projectedAvgPl: Math.round(compositeAvgPl * 100) / 100,
|
|
810
|
-
tradesAffected: bothMatchTrades.length,
|
|
811
|
-
improvement: Math.round(improvement * 100) / 100,
|
|
1023
|
+
// Find trades that match BOTH filters (would be skipped by both)
|
|
1024
|
+
const bothMatchTrades = pool.filter((t) => {
|
|
1025
|
+
const m = t.market as Record<string, unknown>;
|
|
1026
|
+
return testA.test(m) && testB.test(m);
|
|
812
1027
|
});
|
|
1028
|
+
|
|
1029
|
+
if (bothMatchTrades.length < 5) continue;
|
|
1030
|
+
|
|
1031
|
+
const compositeWinRate =
|
|
1032
|
+
(bothMatchTrades.filter((t) => t.trade.pl > 0).length / bothMatchTrades.length) *
|
|
1033
|
+
100;
|
|
1034
|
+
const compositeAvgPl =
|
|
1035
|
+
bothMatchTrades.reduce((sum, t) => sum + t.trade.pl, 0) / bothMatchTrades.length;
|
|
1036
|
+
|
|
1037
|
+
// Only surface if composite win rate is materially better than either standalone filter alone
|
|
1038
|
+
const improvement =
|
|
1039
|
+
compositeWinRate -
|
|
1040
|
+
Math.max(filterA.winRateDelta + baseWinRate, filterB.winRateDelta + baseWinRate);
|
|
1041
|
+
if (improvement > 2) {
|
|
1042
|
+
compositeSuggestions.push({
|
|
1043
|
+
name: `${filterA.filter} AND ${filterB.filter}`,
|
|
1044
|
+
type: "composite",
|
|
1045
|
+
projectedWinRate: Math.round(compositeWinRate * 100) / 100,
|
|
1046
|
+
projectedAvgPl: Math.round(compositeAvgPl * 100) / 100,
|
|
1047
|
+
tradesAffected: bothMatchTrades.length,
|
|
1048
|
+
improvement: Math.round(improvement * 100) / 100,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
813
1051
|
}
|
|
814
1052
|
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// Sort composites by improvement, take top 5
|
|
818
|
-
compositeSuggestions.sort((a, b) => b.improvement - a.improvement);
|
|
819
|
-
const topCompositeSuggestions = compositeSuggestions.slice(0, 5);
|
|
820
1053
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1054
|
+
// Sort composites by improvement, take top 5
|
|
1055
|
+
compositeSuggestions.sort((a, b) => b.improvement - a.improvement);
|
|
1056
|
+
const topCompositeSuggestions = compositeSuggestions.slice(0, 5);
|
|
1057
|
+
|
|
1058
|
+
const summary = `Filter analysis: ${blockId} | ${topSuggestions.length} standalone, ${topCompositeSuggestions.length} composite suggestions (min ${minImprovementPct}% improvement)`;
|
|
1059
|
+
|
|
1060
|
+
const sfResponseData: Record<string, unknown> = {
|
|
1061
|
+
blockId,
|
|
1062
|
+
lagNote:
|
|
1063
|
+
"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.",
|
|
1064
|
+
strategy: effectiveStrategy || null,
|
|
1065
|
+
strategyName: strategyName || null,
|
|
1066
|
+
currentStats: {
|
|
1067
|
+
trades: matchedTrades.length,
|
|
1068
|
+
winRate: Math.round(currentWinRate * 100) / 100,
|
|
1069
|
+
totalPl: Math.round(currentTotalPl * 100) / 100,
|
|
1070
|
+
},
|
|
1071
|
+
suggestedFilters: topSuggestions,
|
|
1072
|
+
compositeSuggestions: topCompositeSuggestions,
|
|
1073
|
+
minImprovementThreshold: minImprovementPct,
|
|
1074
|
+
};
|
|
837
1075
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1076
|
+
if (availabilitySF.warnings.length > 0) {
|
|
1077
|
+
sfResponseData.warnings = availabilitySF.warnings;
|
|
1078
|
+
}
|
|
841
1079
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1080
|
+
// Cross-reference suggestions with profile entry_filters when strategyName provided
|
|
1081
|
+
if (profileEntryFilters && profileEntryFilters.length > 0) {
|
|
1082
|
+
const profileContext: Array<{
|
|
1083
|
+
suggestion: string;
|
|
1084
|
+
field: string;
|
|
1085
|
+
status: "already_in_profile" | "new_suggestion";
|
|
1086
|
+
matchedFilter?: { field: string; operator: string; value: unknown };
|
|
1087
|
+
}> = [];
|
|
1088
|
+
|
|
1089
|
+
for (const suggestion of topSuggestions) {
|
|
1090
|
+
const matchedFilter = profileEntryFilters.find(
|
|
1091
|
+
(f) => f.field === suggestion.condition.field,
|
|
1092
|
+
);
|
|
1093
|
+
profileContext.push({
|
|
1094
|
+
suggestion: suggestion.filter,
|
|
1095
|
+
field: suggestion.condition.field,
|
|
1096
|
+
status: matchedFilter ? "already_in_profile" : "new_suggestion",
|
|
1097
|
+
matchedFilter: matchedFilter
|
|
1098
|
+
? {
|
|
1099
|
+
field: matchedFilter.field,
|
|
1100
|
+
operator: matchedFilter.operator,
|
|
1101
|
+
value: matchedFilter.value,
|
|
1102
|
+
}
|
|
1103
|
+
: undefined,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
850
1106
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
suggestion: suggestion.filter,
|
|
857
|
-
field: suggestion.condition.field,
|
|
858
|
-
status: matchedFilter ? "already_in_profile" : "new_suggestion",
|
|
859
|
-
matchedFilter: matchedFilter
|
|
860
|
-
? { field: matchedFilter.field, operator: matchedFilter.operator, value: matchedFilter.value }
|
|
861
|
-
: undefined,
|
|
862
|
-
});
|
|
1107
|
+
sfResponseData.profile_context = {
|
|
1108
|
+
strategyName,
|
|
1109
|
+
existingFilters: profileEntryFilters.length,
|
|
1110
|
+
crossReference: profileContext,
|
|
1111
|
+
};
|
|
863
1112
|
}
|
|
864
1113
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1114
|
+
return createToolOutput(summary, sfResponseData);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
return {
|
|
1117
|
+
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
|
|
1118
|
+
isError: true,
|
|
869
1119
|
};
|
|
870
1120
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
} catch (error) {
|
|
874
|
-
return {
|
|
875
|
-
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
|
|
876
|
-
isError: true,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
})
|
|
1121
|
+
},
|
|
1122
|
+
),
|
|
880
1123
|
);
|
|
881
1124
|
|
|
882
1125
|
// ---------------------------------------------------------------------------
|
|
@@ -899,239 +1142,278 @@ export function registerMarketDataTools(
|
|
|
899
1142
|
startDate: z.string().optional().describe("Start date filter (YYYY-MM-DD)"),
|
|
900
1143
|
endDate: z.string().optional().describe("End date filter (YYYY-MM-DD)"),
|
|
901
1144
|
ticker: z.string().optional().describe("Underlying ticker symbol (default: SPX)"),
|
|
902
|
-
includeOutcomeFields: z
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1145
|
+
includeOutcomeFields: z
|
|
1146
|
+
.boolean()
|
|
1147
|
+
.default(false)
|
|
1148
|
+
.describe("Include same-day close values (lookahead). Defaults to false for safety."),
|
|
1149
|
+
includeIntradayContext: z
|
|
1150
|
+
.boolean()
|
|
1151
|
+
.default(false)
|
|
1152
|
+
.describe(
|
|
1153
|
+
"Include raw intraday bars from market.spot (intradayBars array with time/open/high/low/close per bar). Requires 1 additional DuckDB query.",
|
|
1154
|
+
),
|
|
1155
|
+
limit: z
|
|
1156
|
+
.number()
|
|
1157
|
+
.min(1)
|
|
1158
|
+
.max(500)
|
|
1159
|
+
.default(50)
|
|
1160
|
+
.describe("Max trades to return (default: 50, max: 500)"),
|
|
907
1161
|
offset: z.number().min(0).default(0).describe("Pagination offset (default: 0)"),
|
|
908
1162
|
}),
|
|
909
1163
|
},
|
|
910
|
-
withFullSync(
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1164
|
+
withFullSync(
|
|
1165
|
+
baseDir,
|
|
1166
|
+
async ({
|
|
1167
|
+
blockId,
|
|
1168
|
+
strategy,
|
|
1169
|
+
startDate,
|
|
1170
|
+
endDate,
|
|
1171
|
+
ticker,
|
|
1172
|
+
includeOutcomeFields,
|
|
1173
|
+
includeIntradayContext,
|
|
1174
|
+
limit,
|
|
1175
|
+
offset,
|
|
1176
|
+
}) => {
|
|
1177
|
+
try {
|
|
1178
|
+
const block = await loadBlock(baseDir, blockId);
|
|
1179
|
+
let trades = block.trades;
|
|
1180
|
+
|
|
1181
|
+
// Filter before paginate
|
|
1182
|
+
trades = filterByStrategy(trades, strategy);
|
|
1183
|
+
trades = filterByDateRange(trades, startDate, endDate);
|
|
1184
|
+
|
|
1185
|
+
if (trades.length === 0) {
|
|
1186
|
+
return {
|
|
1187
|
+
content: [{ type: "text", text: "No trades found matching filters" }],
|
|
1188
|
+
isError: true,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
914
1191
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1192
|
+
const totalTrades = trades.length;
|
|
1193
|
+
const paginated = trades.slice(offset, offset + limit);
|
|
1194
|
+
|
|
1195
|
+
// Short-circuit if offset is past the end (empty page)
|
|
1196
|
+
if (paginated.length === 0) {
|
|
1197
|
+
return createToolOutput(
|
|
1198
|
+
`Enriched trades: ${blockId} | 0/0 matched | offset ${offset}, limit ${limit}`,
|
|
1199
|
+
{
|
|
1200
|
+
blockId,
|
|
1201
|
+
strategy: strategy || null,
|
|
1202
|
+
lagNote: "",
|
|
1203
|
+
tradesTotal: totalTrades,
|
|
1204
|
+
returned: 0,
|
|
1205
|
+
offset,
|
|
1206
|
+
hasMore: false,
|
|
1207
|
+
tradesMatched: 0,
|
|
1208
|
+
unmatchedDates: [],
|
|
1209
|
+
trades: [],
|
|
1210
|
+
},
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
918
1213
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
content: [{ type: "text", text: "No trades found matching filters" }],
|
|
922
|
-
isError: true,
|
|
923
|
-
};
|
|
924
|
-
}
|
|
1214
|
+
// Collect unique ticker+date keys from paginated trades only.
|
|
1215
|
+
const tradeKeys = uniqueTradeLookupKeys(paginated);
|
|
925
1216
|
|
|
926
|
-
|
|
927
|
-
|
|
1217
|
+
// Query DuckDB for lookahead-free market data
|
|
1218
|
+
const conn = await getConnection(baseDir);
|
|
928
1219
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
blockId,
|
|
935
|
-
strategy: strategy || null,
|
|
936
|
-
lagNote: "",
|
|
937
|
-
tradesTotal: totalTrades,
|
|
938
|
-
returned: 0,
|
|
939
|
-
offset,
|
|
940
|
-
hasMore: false,
|
|
941
|
-
tradesMatched: 0,
|
|
942
|
-
unmatchedDates: [],
|
|
943
|
-
trades: [],
|
|
944
|
-
}
|
|
945
|
-
);
|
|
946
|
-
}
|
|
1220
|
+
// Check data availability and collect warnings
|
|
1221
|
+
const resolvedTickerET = normalizeTicker(ticker || "") || DEFAULT_MARKET_TICKER;
|
|
1222
|
+
const availabilityET = await checkDataAvailability(stores, resolvedTickerET, {
|
|
1223
|
+
checkIntraday: includeIntradayContext,
|
|
1224
|
+
});
|
|
947
1225
|
|
|
948
|
-
|
|
949
|
-
|
|
1226
|
+
const { sql: lagSql, params: lagParams } = buildLookaheadFreeQuery(tradeKeys);
|
|
1227
|
+
const dailyResult = await conn.runAndReadAll(lagSql, lagParams);
|
|
1228
|
+
const dailyRecords = resultToRecords(dailyResult);
|
|
1229
|
+
const daily = recordsByTickerDate(dailyRecords);
|
|
1230
|
+
|
|
1231
|
+
// Optionally query outcome (same-day close) data
|
|
1232
|
+
let outcomeMap: Map<string, Record<string, unknown>> | null = null;
|
|
1233
|
+
if (includeOutcomeFields) {
|
|
1234
|
+
const { sql: outcomeSql, params: outcomeParams } = buildOutcomeQuery(tradeKeys);
|
|
1235
|
+
const outcomeResult = await conn.runAndReadAll(outcomeSql, outcomeParams);
|
|
1236
|
+
const outcomeRecords = resultToRecords(outcomeResult);
|
|
1237
|
+
outcomeMap = recordsByTickerDate(outcomeRecords);
|
|
1238
|
+
}
|
|
950
1239
|
|
|
951
|
-
|
|
952
|
-
|
|
1240
|
+
// Optionally query intraday bar data
|
|
1241
|
+
let intradayBarsByKey: Map<
|
|
1242
|
+
string,
|
|
1243
|
+
Array<{ time: string; open: number; high: number; low: number; close: number }>
|
|
1244
|
+
> | null = null;
|
|
1245
|
+
|
|
1246
|
+
if (includeIntradayContext) {
|
|
1247
|
+
// Read intraday bars per (ticker, date) via SpotStore. The store
|
|
1248
|
+
// returns BarRow[] in (date, time) order; we group by ticker+date
|
|
1249
|
+
// so the downstream consumer can index by trade key.
|
|
1250
|
+
intradayBarsByKey = new Map<
|
|
1251
|
+
string,
|
|
1252
|
+
Array<{ time: string; open: number; high: number; low: number; close: number }>
|
|
1253
|
+
>();
|
|
1254
|
+
for (const key of tradeKeys) {
|
|
1255
|
+
const bars = await stores.spot.readBars(key.ticker, key.date, key.date);
|
|
1256
|
+
if (bars.length === 0) continue;
|
|
1257
|
+
const mapKey = marketTickerDateKey(key.ticker, key.date);
|
|
1258
|
+
const list = intradayBarsByKey.get(mapKey) ?? [];
|
|
1259
|
+
for (const bar of bars) {
|
|
1260
|
+
// Defense-in-depth: skip underlying bars with zero/null OHLC
|
|
1261
|
+
// (provider gaps from the spot ingest). Raw bars are left
|
|
1262
|
+
// unfiltered upstream so option tickers can keep legitimate
|
|
1263
|
+
// "no trade" zeros; underlyings always have a real price, so
|
|
1264
|
+
// a zero is a bug that would corrupt downstream high/low/range
|
|
1265
|
+
// computations.
|
|
1266
|
+
if (
|
|
1267
|
+
!Number.isFinite(bar.open) ||
|
|
1268
|
+
bar.open <= 0 ||
|
|
1269
|
+
!Number.isFinite(bar.high) ||
|
|
1270
|
+
bar.high <= 0 ||
|
|
1271
|
+
!Number.isFinite(bar.low) ||
|
|
1272
|
+
bar.low <= 0 ||
|
|
1273
|
+
!Number.isFinite(bar.close) ||
|
|
1274
|
+
bar.close <= 0
|
|
1275
|
+
)
|
|
1276
|
+
continue;
|
|
1277
|
+
list.push({
|
|
1278
|
+
time: String(bar.time),
|
|
1279
|
+
open: Number(bar.open),
|
|
1280
|
+
high: Number(bar.high),
|
|
1281
|
+
low: Number(bar.low),
|
|
1282
|
+
close: Number(bar.close),
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
intradayBarsByKey.set(mapKey, list);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
953
1288
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1289
|
+
// Helper: clean numeric value (BigInt -> Number, NaN -> null)
|
|
1290
|
+
const cleanVal = (val: unknown): unknown => {
|
|
1291
|
+
if (typeof val === "bigint") return Number(val);
|
|
1292
|
+
if (typeof val === "number" && isNaN(val)) return null;
|
|
1293
|
+
return val === undefined ? null : val;
|
|
1294
|
+
};
|
|
957
1295
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1296
|
+
// Build enriched trades
|
|
1297
|
+
const unmatchedDates: string[] = [];
|
|
1298
|
+
let matched = 0;
|
|
1299
|
+
|
|
1300
|
+
const enrichedTrades = paginated.map((trade) => {
|
|
1301
|
+
const lookup = getTradeLookupKey(trade);
|
|
1302
|
+
const marketKey = marketTickerDateKey(lookup.ticker, lookup.date);
|
|
1303
|
+
const marketData = daily.get(marketKey);
|
|
1304
|
+
|
|
1305
|
+
const commissions = trade.openingCommissionsFees + trade.closingCommissionsFees;
|
|
1306
|
+
|
|
1307
|
+
const baseTrade: Record<string, unknown> = {
|
|
1308
|
+
dateOpened: lookup.date,
|
|
1309
|
+
ticker: lookup.ticker,
|
|
1310
|
+
timeOpened: trade.timeOpened,
|
|
1311
|
+
strategy: trade.strategy,
|
|
1312
|
+
legs: trade.legs,
|
|
1313
|
+
pl: trade.pl,
|
|
1314
|
+
numContracts: trade.numContracts,
|
|
1315
|
+
premium: trade.premium,
|
|
1316
|
+
reasonForClose: trade.reasonForClose || null,
|
|
1317
|
+
commissions,
|
|
1318
|
+
};
|
|
962
1319
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const outcomeRecords = resultToRecords(outcomeResult);
|
|
969
|
-
outcomeMap = recordsByTickerDate(outcomeRecords);
|
|
970
|
-
}
|
|
1320
|
+
if (!marketData) {
|
|
1321
|
+
unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
|
|
1322
|
+
baseTrade.entryContext = null;
|
|
1323
|
+
return baseTrade;
|
|
1324
|
+
}
|
|
971
1325
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const bars = await stores.spot.readBars(key.ticker, key.date, key.date);
|
|
982
|
-
if (bars.length === 0) continue;
|
|
983
|
-
const mapKey = marketTickerDateKey(key.ticker, key.date);
|
|
984
|
-
const list = intradayBarsByKey.get(mapKey) ?? [];
|
|
985
|
-
for (const bar of bars) {
|
|
986
|
-
// Defense-in-depth: skip underlying bars with zero/null OHLC
|
|
987
|
-
// (provider gaps from the spot ingest). Raw bars are left
|
|
988
|
-
// unfiltered upstream so option tickers can keep legitimate
|
|
989
|
-
// "no trade" zeros; underlyings always have a real price, so
|
|
990
|
-
// a zero is a bug that would corrupt downstream high/low/range
|
|
991
|
-
// computations.
|
|
992
|
-
if (
|
|
993
|
-
!Number.isFinite(bar.open) || bar.open <= 0 ||
|
|
994
|
-
!Number.isFinite(bar.high) || bar.high <= 0 ||
|
|
995
|
-
!Number.isFinite(bar.low) || bar.low <= 0 ||
|
|
996
|
-
!Number.isFinite(bar.close) || bar.close <= 0
|
|
997
|
-
) continue;
|
|
998
|
-
list.push({
|
|
999
|
-
time: String(bar.time),
|
|
1000
|
-
open: Number(bar.open),
|
|
1001
|
-
high: Number(bar.high),
|
|
1002
|
-
low: Number(bar.low),
|
|
1003
|
-
close: Number(bar.close),
|
|
1004
|
-
});
|
|
1326
|
+
matched++;
|
|
1327
|
+
|
|
1328
|
+
// Build sameDay: open-known + static fields
|
|
1329
|
+
const sameDay: Record<string, unknown> = {};
|
|
1330
|
+
for (const field of OPEN_KNOWN_FIELDS) {
|
|
1331
|
+
sameDay[field] = cleanVal(marketData[field]);
|
|
1332
|
+
}
|
|
1333
|
+
for (const field of STATIC_FIELDS) {
|
|
1334
|
+
sameDay[field] = cleanVal(marketData[field]);
|
|
1005
1335
|
}
|
|
1006
|
-
intradayBarsByKey.set(mapKey, list);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
1336
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
};
|
|
1337
|
+
// Build priorDay: close-derived fields (read prev_* columns)
|
|
1338
|
+
const priorDay: Record<string, unknown> = {};
|
|
1339
|
+
for (const field of CLOSE_KNOWN_FIELDS) {
|
|
1340
|
+
priorDay[field] = cleanVal(marketData[`prev_${field}`]);
|
|
1341
|
+
}
|
|
1016
1342
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1343
|
+
baseTrade.entryContext = { sameDay, priorDay };
|
|
1344
|
+
|
|
1345
|
+
// Outcome fields (opt-in, same-day close values)
|
|
1346
|
+
if (includeOutcomeFields && outcomeMap) {
|
|
1347
|
+
const outcomeData = outcomeMap.get(marketKey);
|
|
1348
|
+
if (outcomeData) {
|
|
1349
|
+
const outcomeFields: Record<string, unknown> = {};
|
|
1350
|
+
for (const field of CLOSE_KNOWN_FIELDS) {
|
|
1351
|
+
outcomeFields[field] = cleanVal(outcomeData[field]);
|
|
1352
|
+
}
|
|
1353
|
+
baseTrade.outcomeFields = outcomeFields;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1020
1356
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const commissions = trade.openingCommissionsFees + trade.closingCommissionsFees;
|
|
1027
|
-
|
|
1028
|
-
const baseTrade: Record<string, unknown> = {
|
|
1029
|
-
dateOpened: lookup.date,
|
|
1030
|
-
ticker: lookup.ticker,
|
|
1031
|
-
timeOpened: trade.timeOpened,
|
|
1032
|
-
strategy: trade.strategy,
|
|
1033
|
-
legs: trade.legs,
|
|
1034
|
-
pl: trade.pl,
|
|
1035
|
-
numContracts: trade.numContracts,
|
|
1036
|
-
premium: trade.premium,
|
|
1037
|
-
reasonForClose: trade.reasonForClose || null,
|
|
1038
|
-
commissions,
|
|
1039
|
-
};
|
|
1357
|
+
// Intraday bars (opt-in, raw bar arrays per ticker+date)
|
|
1358
|
+
if (includeIntradayContext && intradayBarsByKey) {
|
|
1359
|
+
const bars = intradayBarsByKey.get(marketKey) || null;
|
|
1360
|
+
baseTrade.intradayBars = bars;
|
|
1361
|
+
}
|
|
1040
1362
|
|
|
1041
|
-
if (!marketData) {
|
|
1042
|
-
unmatchedDates.push(`${lookup.date}|${lookup.ticker}`);
|
|
1043
|
-
baseTrade.entryContext = null;
|
|
1044
1363
|
return baseTrade;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
matched++;
|
|
1364
|
+
});
|
|
1048
1365
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1366
|
+
const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
|
|
1367
|
+
|
|
1368
|
+
// Build lagNote
|
|
1369
|
+
let lagNote =
|
|
1370
|
+
"Entry context uses lookahead-free temporal joins on ticker+date via market.enriched + market.spot_daily LEFT JOIN VIX tickers + market.enriched_context. " +
|
|
1371
|
+
"Same-day (open-known) fields: Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Month, Is_Opex. " +
|
|
1372
|
+
"Prior-day (close-derived) fields: VIX_Close, RSI_14, Vol_Regime, Realized_Vol_5D, Realized_Vol_20D, VIX_IVR, VIX_IVP, Term_Structure_State, etc. — " +
|
|
1373
|
+
"use the previous trading day's close-derived values to prevent lookahead bias.";
|
|
1374
|
+
if (includeOutcomeFields) {
|
|
1375
|
+
lagNote +=
|
|
1376
|
+
" Outcome fields contain same-day close-derived values and represent information NOT available at trade entry time.";
|
|
1053
1377
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
// Build priorDay: close-derived fields (read prev_* columns)
|
|
1059
|
-
const priorDay: Record<string, unknown> = {};
|
|
1060
|
-
for (const field of CLOSE_KNOWN_FIELDS) {
|
|
1061
|
-
priorDay[field] = cleanVal(marketData[`prev_${field}`]);
|
|
1378
|
+
if (includeIntradayContext) {
|
|
1379
|
+
lagNote +=
|
|
1380
|
+
" intradayBars contains raw OHLC bar arrays from market.spot keyed by ticker+date.";
|
|
1062
1381
|
}
|
|
1063
1382
|
|
|
1064
|
-
|
|
1383
|
+
// Build response data
|
|
1384
|
+
const responseData: Record<string, unknown> = {
|
|
1385
|
+
blockId,
|
|
1386
|
+
strategy: strategy || null,
|
|
1387
|
+
lagNote,
|
|
1388
|
+
tradesTotal: totalTrades,
|
|
1389
|
+
returned: paginated.length,
|
|
1390
|
+
offset,
|
|
1391
|
+
hasMore: offset + limit < totalTrades,
|
|
1392
|
+
tradesMatched: matched,
|
|
1393
|
+
unmatchedDates: sortedUnmatchedDates,
|
|
1394
|
+
trades: enrichedTrades,
|
|
1395
|
+
};
|
|
1065
1396
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (outcomeData) {
|
|
1070
|
-
const outcomeFields: Record<string, unknown> = {};
|
|
1071
|
-
for (const field of CLOSE_KNOWN_FIELDS) {
|
|
1072
|
-
outcomeFields[field] = cleanVal(outcomeData[field]);
|
|
1073
|
-
}
|
|
1074
|
-
baseTrade.outcomeFields = outcomeFields;
|
|
1075
|
-
}
|
|
1397
|
+
if (includeOutcomeFields) {
|
|
1398
|
+
responseData.lookaheadWarning =
|
|
1399
|
+
"WARNING: outcomeFields contain same-day close-derived values that were NOT available at trade entry time. Do not use these for entry signal analysis.";
|
|
1076
1400
|
}
|
|
1077
1401
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const bars = intradayBarsByKey.get(marketKey) || null;
|
|
1081
|
-
baseTrade.intradayBars = bars;
|
|
1402
|
+
if (availabilityET.warnings.length > 0) {
|
|
1403
|
+
responseData.warnings = availabilityET.warnings;
|
|
1082
1404
|
}
|
|
1083
1405
|
|
|
1084
|
-
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
const sortedUnmatchedDates = [...new Set(unmatchedDates)].sort();
|
|
1088
|
-
|
|
1089
|
-
// Build lagNote
|
|
1090
|
-
let lagNote =
|
|
1091
|
-
"Entry context uses lookahead-free temporal joins on ticker+date via market.enriched + market.spot_daily LEFT JOIN VIX tickers + market.enriched_context. " +
|
|
1092
|
-
"Same-day (open-known) fields: Gap_Pct, VIX_Open, VIX_Gap_Pct, Prior_Range_vs_ATR, Day_of_Week, Month, Is_Opex. " +
|
|
1093
|
-
"Prior-day (close-derived) fields: VIX_Close, RSI_14, Vol_Regime, Realized_Vol_5D, Realized_Vol_20D, VIX_IVR, VIX_IVP, Term_Structure_State, etc. — " +
|
|
1094
|
-
"use the previous trading day's close-derived values to prevent lookahead bias.";
|
|
1095
|
-
if (includeOutcomeFields) {
|
|
1096
|
-
lagNote += " Outcome fields contain same-day close-derived values and represent information NOT available at trade entry time.";
|
|
1097
|
-
}
|
|
1098
|
-
if (includeIntradayContext) {
|
|
1099
|
-
lagNote += " intradayBars contains raw OHLC bar arrays from market.spot keyed by ticker+date.";
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Build response data
|
|
1103
|
-
const responseData: Record<string, unknown> = {
|
|
1104
|
-
blockId,
|
|
1105
|
-
strategy: strategy || null,
|
|
1106
|
-
lagNote,
|
|
1107
|
-
tradesTotal: totalTrades,
|
|
1108
|
-
returned: paginated.length,
|
|
1109
|
-
offset,
|
|
1110
|
-
hasMore: offset + limit < totalTrades,
|
|
1111
|
-
tradesMatched: matched,
|
|
1112
|
-
unmatchedDates: sortedUnmatchedDates,
|
|
1113
|
-
trades: enrichedTrades,
|
|
1114
|
-
};
|
|
1115
|
-
|
|
1116
|
-
if (includeOutcomeFields) {
|
|
1117
|
-
responseData.lookaheadWarning =
|
|
1118
|
-
"WARNING: outcomeFields contain same-day close-derived values that were NOT available at trade entry time. Do not use these for entry signal analysis.";
|
|
1119
|
-
}
|
|
1406
|
+
const summary = `Enriched trades: ${blockId} | ${matched}/${paginated.length} matched | offset ${offset}, limit ${limit}`;
|
|
1120
1407
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1408
|
+
return createToolOutput(summary, responseData);
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
return {
|
|
1411
|
+
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
|
|
1412
|
+
isError: true,
|
|
1413
|
+
};
|
|
1123
1414
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
return createToolOutput(summary, responseData);
|
|
1128
|
-
} catch (error) {
|
|
1129
|
-
return {
|
|
1130
|
-
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
|
|
1131
|
-
isError: true,
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
})
|
|
1415
|
+
},
|
|
1416
|
+
),
|
|
1135
1417
|
);
|
|
1136
1418
|
|
|
1137
1419
|
// ---------------------------------------------------------------------------
|
|
@@ -1161,245 +1443,273 @@ export function registerMarketDataTools(
|
|
|
1161
1443
|
ticker: z.string().default("SPX").describe("Ticker symbol (default: SPX)"),
|
|
1162
1444
|
startDate: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
1163
1445
|
endDate: z.string().optional().describe("End date (YYYY-MM-DD, defaults to today)"),
|
|
1164
|
-
startTime: z
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1446
|
+
startTime: z
|
|
1447
|
+
.string()
|
|
1448
|
+
.default("0930")
|
|
1449
|
+
.describe("ORB window start in HHMM format (default: '0930')"),
|
|
1450
|
+
endTime: z
|
|
1451
|
+
.string()
|
|
1452
|
+
.default("1000")
|
|
1453
|
+
.describe("ORB window end in HHMM format (default: '1000')"),
|
|
1454
|
+
useHighLow: z
|
|
1455
|
+
.boolean()
|
|
1456
|
+
.default(true)
|
|
1457
|
+
.describe("Use high/low for range (true) or close prices (false)"),
|
|
1458
|
+
barResolution: z
|
|
1459
|
+
.string()
|
|
1460
|
+
.optional()
|
|
1461
|
+
.describe("Expected bar resolution in minutes (e.g., '15'). Auto-detected if omitted."),
|
|
1168
1462
|
limit: z.number().min(1).max(500).default(100).describe("Max days to return"),
|
|
1169
1463
|
}),
|
|
1170
1464
|
},
|
|
1171
|
-
withFullSync(
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
normalizedTicker,
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1465
|
+
withFullSync(
|
|
1466
|
+
baseDir,
|
|
1467
|
+
async ({
|
|
1468
|
+
ticker,
|
|
1469
|
+
startDate,
|
|
1470
|
+
endDate,
|
|
1471
|
+
startTime,
|
|
1472
|
+
endTime,
|
|
1473
|
+
useHighLow,
|
|
1474
|
+
barResolution,
|
|
1475
|
+
limit,
|
|
1476
|
+
}) => {
|
|
1477
|
+
try {
|
|
1478
|
+
const normalizedTicker = normalizeTicker(ticker) || "SPX";
|
|
1479
|
+
const end = endDate || new Date().toISOString().split("T")[0];
|
|
1480
|
+
|
|
1481
|
+
// Convert HHMM to HH:MM for SQL comparison
|
|
1482
|
+
const sqlStartTime = hhmmToSqlTime(startTime);
|
|
1483
|
+
const sqlEndTime = hhmmToSqlTime(endTime);
|
|
1484
|
+
|
|
1485
|
+
// Check data availability
|
|
1486
|
+
const availability = await checkDataAvailability(stores, normalizedTicker, {
|
|
1487
|
+
checkIntraday: true,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
// Data availability + bar reads flow through SpotStore; the window
|
|
1491
|
+
// aggregation and breakout-detection logic runs in TypeScript over
|
|
1492
|
+
// the BarRow[] returned by readBars.
|
|
1493
|
+
|
|
1494
|
+
// Quick check: is there any spot data for this ticker over the requested range?
|
|
1495
|
+
// Use a wide bracket so the "no data at all" case is distinguishable from
|
|
1496
|
+
// "no data in the requested range" (the latter is handled below by an empty
|
|
1497
|
+
// `byDate` map).
|
|
1498
|
+
const wideCoverage = await stores.spot.getCoverage(
|
|
1499
|
+
normalizedTicker,
|
|
1500
|
+
"2000-01-01",
|
|
1501
|
+
new Date().toISOString().split("T")[0],
|
|
1502
|
+
);
|
|
1503
|
+
if (wideCoverage.totalDates === 0) {
|
|
1504
|
+
return createToolOutput(`ORB (${normalizedTicker}): No intraday data available`, {
|
|
1200
1505
|
query: { ticker: normalizedTicker, startTime, endTime, startDate, endDate: end },
|
|
1201
1506
|
warnings: availability.warnings,
|
|
1202
1507
|
days: [],
|
|
1203
1508
|
stats: { totalDays: 0 },
|
|
1204
|
-
}
|
|
1205
|
-
);
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// Determine bar resolution: use provided value or auto-detect from first available date
|
|
1209
|
-
let resolvedBarResolution: number | null = null;
|
|
1210
|
-
if (barResolution !== undefined && barResolution !== null) {
|
|
1211
|
-
const parsed = parseInt(barResolution, 10);
|
|
1212
|
-
if (!isNaN(parsed) && parsed > 0) {
|
|
1213
|
-
resolvedBarResolution = parsed;
|
|
1509
|
+
});
|
|
1214
1510
|
}
|
|
1215
|
-
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
wideCoverage.earliest,
|
|
1223
|
-
wideCoverage.earliest,
|
|
1224
|
-
);
|
|
1225
|
-
const distinctTimes: string[] = [];
|
|
1226
|
-
for (const bar of sampleBars) {
|
|
1227
|
-
const t = String(bar.time);
|
|
1228
|
-
if (distinctTimes.length === 0 || distinctTimes[distinctTimes.length - 1] !== t) {
|
|
1229
|
-
distinctTimes.push(t);
|
|
1230
|
-
if (distinctTimes.length === 10) break;
|
|
1231
|
-
}
|
|
1511
|
+
|
|
1512
|
+
// Determine bar resolution: use provided value or auto-detect from first available date
|
|
1513
|
+
let resolvedBarResolution: number | null = null;
|
|
1514
|
+
if (barResolution !== undefined && barResolution !== null) {
|
|
1515
|
+
const parsed = parseInt(barResolution, 10);
|
|
1516
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
1517
|
+
resolvedBarResolution = parsed;
|
|
1232
1518
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1519
|
+
} else if (wideCoverage.earliest) {
|
|
1520
|
+
// Auto-detect: read the first available date's bars, compute the gap
|
|
1521
|
+
// between the first two distinct times. SpotStore returns rows ordered
|
|
1522
|
+
// by (date, time) so we can scan in order.
|
|
1523
|
+
try {
|
|
1524
|
+
const sampleBars = await stores.spot.readBars(
|
|
1525
|
+
normalizedTicker,
|
|
1526
|
+
wideCoverage.earliest,
|
|
1527
|
+
wideCoverage.earliest,
|
|
1528
|
+
);
|
|
1529
|
+
const distinctTimes: string[] = [];
|
|
1530
|
+
for (const bar of sampleBars) {
|
|
1531
|
+
const t = String(bar.time);
|
|
1532
|
+
if (distinctTimes.length === 0 || distinctTimes[distinctTimes.length - 1] !== t) {
|
|
1533
|
+
distinctTimes.push(t);
|
|
1534
|
+
if (distinctTimes.length === 10) break;
|
|
1535
|
+
}
|
|
1241
1536
|
}
|
|
1537
|
+
if (distinctTimes.length >= 2) {
|
|
1538
|
+
const t1 = distinctTimes[0];
|
|
1539
|
+
const t2 = distinctTimes[1];
|
|
1540
|
+
const [h1, m1] = t1.split(":").map(Number);
|
|
1541
|
+
const [h2, m2] = t2.split(":").map(Number);
|
|
1542
|
+
const gap = h2 * 60 + m2 - (h1 * 60 + m1);
|
|
1543
|
+
if (gap > 0) {
|
|
1544
|
+
resolvedBarResolution = gap;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
} catch {
|
|
1548
|
+
// If auto-detection fails, proceed without resolution filtering
|
|
1242
1549
|
}
|
|
1243
|
-
} catch {
|
|
1244
|
-
// If auto-detection fails, proceed without resolution filtering
|
|
1245
1550
|
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// Read all in-range bars and group by date for ORB computation. The
|
|
1249
|
-
// `useHighLow` toggle picks raw high/low vs close-of-bar high/low.
|
|
1250
|
-
const allBars = await stores.spot.readBars(normalizedTicker, startDate, end);
|
|
1251
|
-
|
|
1252
|
-
// Group by date, preserving (time)-ascending order from readBars.
|
|
1253
|
-
// Defense-in-depth: skip underlying bars with zero/null OHLC (provider
|
|
1254
|
-
// gaps from the spot ingest). Without this, a zero-low minute in the
|
|
1255
|
-
// opening window would corrupt ORB_Low and the breakout/range output.
|
|
1256
|
-
const barsByDate = new Map<string, typeof allBars>();
|
|
1257
|
-
for (const bar of allBars) {
|
|
1258
|
-
if (
|
|
1259
|
-
!Number.isFinite(bar.open) || bar.open <= 0 ||
|
|
1260
|
-
!Number.isFinite(bar.high) || bar.high <= 0 ||
|
|
1261
|
-
!Number.isFinite(bar.low) || bar.low <= 0 ||
|
|
1262
|
-
!Number.isFinite(bar.close) || bar.close <= 0
|
|
1263
|
-
) continue;
|
|
1264
|
-
const arr = barsByDate.get(bar.date);
|
|
1265
|
-
if (arr) arr.push(bar);
|
|
1266
|
-
else barsByDate.set(bar.date, [bar]);
|
|
1267
|
-
}
|
|
1268
1551
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
);
|
|
1294
|
-
if (windowBars.length === 0) continue;
|
|
1295
|
-
|
|
1296
|
-
let orbHigh = -Infinity;
|
|
1297
|
-
let orbLow = Infinity;
|
|
1298
|
-
let orbOpen: number | null = null;
|
|
1299
|
-
for (const b of windowBars) {
|
|
1300
|
-
const hi = useHighLow ? Number(b.high) : Number(b.close);
|
|
1301
|
-
const lo = useHighLow ? Number(b.low) : Number(b.close);
|
|
1302
|
-
if (hi > orbHigh) orbHigh = hi;
|
|
1303
|
-
if (lo < orbLow) orbLow = lo;
|
|
1304
|
-
if (String(b.time) === sqlStartTime && orbOpen === null) {
|
|
1305
|
-
orbOpen = Number(b.open);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
const orbRange = orbHigh - orbLow;
|
|
1309
|
-
const orbRangePct = orbLow > 0 ? (orbRange / orbLow) * 100 : 0;
|
|
1310
|
-
|
|
1311
|
-
// Breakout window: time strictly > sqlEndTime
|
|
1312
|
-
let breakoutUpTime: string | null = null;
|
|
1313
|
-
let breakoutDownTime: string | null = null;
|
|
1314
|
-
for (const b of dayBars) {
|
|
1315
|
-
const t = String(b.time);
|
|
1316
|
-
if (t <= sqlEndTime) continue;
|
|
1317
|
-
const upHit = useHighLow ? Number(b.high) > orbHigh : Number(b.close) > orbHigh;
|
|
1318
|
-
const downHit = useHighLow ? Number(b.low) < orbLow : Number(b.close) < orbLow;
|
|
1319
|
-
if (upHit && breakoutUpTime === null) breakoutUpTime = t;
|
|
1320
|
-
if (downHit && breakoutDownTime === null) breakoutDownTime = t;
|
|
1321
|
-
if (breakoutUpTime !== null && breakoutDownTime !== null) break;
|
|
1552
|
+
// Read all in-range bars and group by date for ORB computation. The
|
|
1553
|
+
// `useHighLow` toggle picks raw high/low vs close-of-bar high/low.
|
|
1554
|
+
const allBars = await stores.spot.readBars(normalizedTicker, startDate, end);
|
|
1555
|
+
|
|
1556
|
+
// Group by date, preserving (time)-ascending order from readBars.
|
|
1557
|
+
// Defense-in-depth: skip underlying bars with zero/null OHLC (provider
|
|
1558
|
+
// gaps from the spot ingest). Without this, a zero-low minute in the
|
|
1559
|
+
// opening window would corrupt ORB_Low and the breakout/range output.
|
|
1560
|
+
const barsByDate = new Map<string, typeof allBars>();
|
|
1561
|
+
for (const bar of allBars) {
|
|
1562
|
+
if (
|
|
1563
|
+
!Number.isFinite(bar.open) ||
|
|
1564
|
+
bar.open <= 0 ||
|
|
1565
|
+
!Number.isFinite(bar.high) ||
|
|
1566
|
+
bar.high <= 0 ||
|
|
1567
|
+
!Number.isFinite(bar.low) ||
|
|
1568
|
+
bar.low <= 0 ||
|
|
1569
|
+
!Number.isFinite(bar.close) ||
|
|
1570
|
+
bar.close <= 0
|
|
1571
|
+
)
|
|
1572
|
+
continue;
|
|
1573
|
+
const arr = barsByDate.get(bar.date);
|
|
1574
|
+
if (arr) arr.push(bar);
|
|
1575
|
+
else barsByDate.set(bar.date, [bar]);
|
|
1322
1576
|
}
|
|
1323
1577
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1578
|
+
type BreakoutCondition = "HighFirst" | "LowFirst" | "HighOnly" | "LowOnly" | "NoBreakout";
|
|
1579
|
+
|
|
1580
|
+
interface OrbDayResult {
|
|
1581
|
+
date: string;
|
|
1582
|
+
ORB_High: number;
|
|
1583
|
+
ORB_Low: number;
|
|
1584
|
+
ORB_Range: number;
|
|
1585
|
+
ORB_Range_Pct: number;
|
|
1586
|
+
ORB_Open: number | null;
|
|
1587
|
+
breakout_condition: BreakoutCondition;
|
|
1588
|
+
breakout_up_time: string | null;
|
|
1589
|
+
breakout_down_time: string | null;
|
|
1590
|
+
entry_triggered: boolean;
|
|
1333
1591
|
}
|
|
1334
1592
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1593
|
+
// Compute ORB window + breakouts per date in TypeScript over the
|
|
1594
|
+
// grouped bars.
|
|
1595
|
+
const days: OrbDayResult[] = [];
|
|
1596
|
+
const sortedDates = [...barsByDate.keys()].sort();
|
|
1597
|
+
for (const date of sortedDates) {
|
|
1598
|
+
const dayBars = barsByDate.get(date)!;
|
|
1599
|
+
// ORB window: time in [sqlStartTime, sqlEndTime]
|
|
1600
|
+
const windowBars = dayBars.filter(
|
|
1601
|
+
(b) => String(b.time) >= sqlStartTime && String(b.time) <= sqlEndTime,
|
|
1602
|
+
);
|
|
1603
|
+
if (windowBars.length === 0) continue;
|
|
1604
|
+
|
|
1605
|
+
let orbHigh = -Infinity;
|
|
1606
|
+
let orbLow = Infinity;
|
|
1607
|
+
let orbOpen: number | null = null;
|
|
1608
|
+
for (const b of windowBars) {
|
|
1609
|
+
const hi = useHighLow ? Number(b.high) : Number(b.close);
|
|
1610
|
+
const lo = useHighLow ? Number(b.low) : Number(b.close);
|
|
1611
|
+
if (hi > orbHigh) orbHigh = hi;
|
|
1612
|
+
if (lo < orbLow) orbLow = lo;
|
|
1613
|
+
if (String(b.time) === sqlStartTime && orbOpen === null) {
|
|
1614
|
+
orbOpen = Number(b.open);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
const orbRange = orbHigh - orbLow;
|
|
1618
|
+
const orbRangePct = orbLow > 0 ? (orbRange / orbLow) * 100 : 0;
|
|
1619
|
+
|
|
1620
|
+
// Breakout window: time strictly > sqlEndTime
|
|
1621
|
+
let breakoutUpTime: string | null = null;
|
|
1622
|
+
let breakoutDownTime: string | null = null;
|
|
1623
|
+
for (const b of dayBars) {
|
|
1624
|
+
const t = String(b.time);
|
|
1625
|
+
if (t <= sqlEndTime) continue;
|
|
1626
|
+
const upHit = useHighLow ? Number(b.high) > orbHigh : Number(b.close) > orbHigh;
|
|
1627
|
+
const downHit = useHighLow ? Number(b.low) < orbLow : Number(b.close) < orbLow;
|
|
1628
|
+
if (upHit && breakoutUpTime === null) breakoutUpTime = t;
|
|
1629
|
+
if (downHit && breakoutDownTime === null) breakoutDownTime = t;
|
|
1630
|
+
if (breakoutUpTime !== null && breakoutDownTime !== null) break;
|
|
1631
|
+
}
|
|
1348
1632
|
|
|
1349
|
-
|
|
1633
|
+
let breakoutCondition: BreakoutCondition;
|
|
1634
|
+
if (breakoutUpTime !== null && breakoutDownTime !== null) {
|
|
1635
|
+
breakoutCondition = breakoutUpTime < breakoutDownTime ? "HighFirst" : "LowFirst";
|
|
1636
|
+
} else if (breakoutUpTime !== null) {
|
|
1637
|
+
breakoutCondition = "HighOnly";
|
|
1638
|
+
} else if (breakoutDownTime !== null) {
|
|
1639
|
+
breakoutCondition = "LowOnly";
|
|
1640
|
+
} else {
|
|
1641
|
+
breakoutCondition = "NoBreakout";
|
|
1642
|
+
}
|
|
1350
1643
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1644
|
+
days.push({
|
|
1645
|
+
date,
|
|
1646
|
+
ORB_High: Math.round(orbHigh * 100) / 100,
|
|
1647
|
+
ORB_Low: Math.round(orbLow * 100) / 100,
|
|
1648
|
+
ORB_Range: Math.round(orbRange * 100) / 100,
|
|
1649
|
+
ORB_Range_Pct: Math.round(orbRangePct * 10000) / 10000,
|
|
1650
|
+
ORB_Open: orbOpen !== null ? Math.round(orbOpen * 100) / 100 : null,
|
|
1651
|
+
breakout_condition: breakoutCondition,
|
|
1652
|
+
breakout_up_time: breakoutUpTime,
|
|
1653
|
+
breakout_down_time: breakoutDownTime,
|
|
1654
|
+
entry_triggered: breakoutCondition !== "NoBreakout",
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1355
1657
|
|
|
1356
|
-
|
|
1357
|
-
HighFirst: days.filter((d) => d.breakout_condition === "HighFirst").length,
|
|
1358
|
-
LowFirst: days.filter((d) => d.breakout_condition === "LowFirst").length,
|
|
1359
|
-
HighOnly: days.filter((d) => d.breakout_condition === "HighOnly").length,
|
|
1360
|
-
LowOnly: days.filter((d) => d.breakout_condition === "LowOnly").length,
|
|
1361
|
-
NoBreakout: days.filter((d) => d.breakout_condition === "NoBreakout").length,
|
|
1362
|
-
};
|
|
1658
|
+
const totalDays = days.length;
|
|
1363
1659
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1660
|
+
// Compute aggregate stats
|
|
1661
|
+
const avgOrbRangePct =
|
|
1662
|
+
totalDays > 0 ? days.reduce((sum, d) => sum + d.ORB_Range_Pct, 0) / totalDays : 0;
|
|
1366
1663
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1664
|
+
const breakdownByCondition = {
|
|
1665
|
+
HighFirst: days.filter((d) => d.breakout_condition === "HighFirst").length,
|
|
1666
|
+
LowFirst: days.filter((d) => d.breakout_condition === "LowFirst").length,
|
|
1667
|
+
HighOnly: days.filter((d) => d.breakout_condition === "HighOnly").length,
|
|
1668
|
+
LowOnly: days.filter((d) => d.breakout_condition === "LowOnly").length,
|
|
1669
|
+
NoBreakout: days.filter((d) => d.breakout_condition === "NoBreakout").length,
|
|
1670
|
+
};
|
|
1370
1671
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
endTime
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1672
|
+
// Apply limit
|
|
1673
|
+
const limitedDays = days.slice(0, limit);
|
|
1674
|
+
|
|
1675
|
+
const summary =
|
|
1676
|
+
`ORB (${normalizedTicker}, ${startTime}-${endTime}): ${startDate} to ${end} | ` +
|
|
1677
|
+
`${totalDays} days, avg range ${formatPercent(avgOrbRangePct)}`;
|
|
1678
|
+
|
|
1679
|
+
const responseData: Record<string, unknown> = {
|
|
1680
|
+
query: {
|
|
1681
|
+
ticker: normalizedTicker,
|
|
1682
|
+
startTime,
|
|
1683
|
+
endTime,
|
|
1684
|
+
sqlStartTime,
|
|
1685
|
+
sqlEndTime,
|
|
1686
|
+
startDate,
|
|
1687
|
+
endDate: end,
|
|
1688
|
+
useHighLow,
|
|
1689
|
+
barResolution:
|
|
1690
|
+
resolvedBarResolution !== null ? String(resolvedBarResolution) : "auto",
|
|
1691
|
+
},
|
|
1692
|
+
stats: {
|
|
1693
|
+
totalDays,
|
|
1694
|
+
avgOrbRangePct: Math.round(avgOrbRangePct * 10000) / 10000,
|
|
1695
|
+
breakdownByCondition,
|
|
1696
|
+
},
|
|
1697
|
+
returned: limitedDays.length,
|
|
1698
|
+
days: limitedDays,
|
|
1699
|
+
};
|
|
1391
1700
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1701
|
+
if (availability.warnings.length > 0) {
|
|
1702
|
+
responseData.warnings = availability.warnings;
|
|
1703
|
+
}
|
|
1395
1704
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1705
|
+
return createToolOutput(summary, responseData);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
return {
|
|
1708
|
+
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
|
|
1709
|
+
isError: true,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
},
|
|
1713
|
+
),
|
|
1404
1714
|
);
|
|
1405
1715
|
}
|