import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { DuckDBConnection } from '@duckdb/node-api'; type EntrySource = "default" | "user" | "user-override"; interface TickerEntry { underlying: string; roots: string[]; source: EntrySource; } declare class TickerRegistry { private rootMap; private entries; private readonly bundledDefaults; constructor(defaults: Array<{ underlying: string; roots: string[]; }>, userOverrides?: Array<{ underlying: string; roots: string[]; }>); /** * Resolve a root symbol to its underlying. * Identity fallback (returns the root unchanged) when unknown — unknown * roots are treated as their own underlying so single-symbol tickers * (e.g. leveraged ETFs) keep working without explicit registration. */ resolve(root: string): string; /** * Add or update an underlying entry. * - New underlying (not a bundled default): source = "user" * - Overriding a bundled default: source = "user-override" * * @throws on invalid characters in `underlying` or any `root` (defense-in-depth — see file header). */ register(entry: { underlying: string; roots: string[]; }): TickerEntry; /** * Remove a user entry, or revert a user-override to its bundled default. * Bundled defaults cannot be removed. * * @throws on unknown underlying or when attempting to remove a bundled default. */ unregister(underlying: string): void; /** Return all entries (defaults + user + user-override) as defensive copies. */ list(): TickerEntry[]; /** * Serialize ONLY user + user-override entries. * Bundled defaults are NEVER persisted — they live in defaults.json and ship with the binary. */ toJSON(): { version: 1; underlyings: Array<{ underlying: string; roots: string[]; }>; }; } /** * Market Data Provider Interface * * Defines the shared types and provider abstraction for fetching market data * from external APIs (Massive.com, ThetaData, etc.). * * All providers normalize their responses to BarRow and OptionContract types. * The factory function getProvider() selects the active provider based on the * MARKET_DATA_PROVIDER environment variable (default: "massive"). */ /** Normalized OHLCV bar — shared output type for all providers. */ interface BarRow { date: string; open: number; high: number; low: number; close: number; volume: number; ticker: string; time?: string; bid?: number; ask?: number; } /** * chain-loader.ts * * Pure helpers for option chain filtering and deduplication. * * The three-step cache-lifecycle fetch path is gone — reads never trigger * provider fetches. Per-date chain reads now flow through * `stores.chain.readChain(underlying, date)` (ChainStore API). Empty array * is the skip signal — the legacy `ChainSkipResult` / `isChainSkip` * type-guard pair has been deleted along with the SQL builders that backed * the cache lookups. * * Surviving public surface (this file): * - filterChain(contracts, filter) pure DTE / contract-type filter * - deduplicateContracts(contracts) pure SPX/SPXW collision resolver * - ContractRow type single source of truth for the * on-the-wire contract shape (also * re-exported from market/stores/types.ts) * * Transitional surface (deprecated, scheduled for removal): * - ChainLoadResult interface { contracts: ContractRow[], source: 'cache' } * preserved until downstream consumers * are rewritten to accept `ContractRow[]` * directly. * * Anything not listed above (loadChain, loadChainsBulk, buildCachedChainQuery, * optionChainPartitionSource, chainColumnsSql, chainRowFromSql, ChainResult, * ChainSkipResult, ChainSkipReason, isChainSkip) was deleted as part of the * ChainStore migration. */ interface ContractRow { underlying: string; date: string; ticker: string; contract_type: "call" | "put"; strike: number; expiration: string; dte: number; exercise_style: string; } type GreekColumn = "delta" | "gamma" | "theta" | "vega" | "iv"; /** * Shared types for the Market Data 3.0 store layer. * * Phase 1: Types only; concrete store backends ship in Phase 2. * This file is shared code — private-only modules MUST NOT be imported here. * * Note on BarRow / ContractRow: * Both types already exist in shared code under `src/utils/` (see * `src/utils/market-provider.ts` for `BarRow` and `src/utils/chain-loader.ts` for * `ContractRow`). We re-export them from here to keep a single source of truth and * to satisfy the shared-code-no-private-import rule. Phase 2 concrete stores and * downstream plans import these from `src/market/stores/types.js`. */ /** * StoreContext — one per MCP process (or standalone script). * * Locked to exactly four fields per CONTEXT.md D-03. `parquetMode` is a snapshot * taken at construction time (see RESEARCH.md Pitfall 8 — do not re-read the env var * mid-process, since concrete stores may cache backend choice). */ interface StoreContext { conn: DuckDBConnection; dataDir: string; parquetMode: boolean; tickers: TickerRegistry; } /** * Option quote snapshot at a given minute. * * The quote store persists one row per (occ_ticker, timestamp) minute. * Phase 2 backends may choose wider schemas on disk, but this is the in-memory * contract every reader/writer agrees on. */ interface QuoteRow { occ_ticker: string; timestamp: string; bid: number; ask: number; bid_size?: number; ask_size?: number; /** See MinuteQuote.source for semantics. Persisted to option_quote_minutes.source. */ source?: "nbbo" | "synth_close" | null; delta?: number | null; gamma?: number | null; theta?: number | null; vega?: number | null; iv?: number | null; greeks_source?: "massive" | "thetadata" | "computed" | null; greeks_revision?: number | null; rate_type?: string | null; rate_value?: number | null; gamma_source?: string | null; } /** * Daily open-interest snapshot for a single option contract. * * The OI store persists one row per (occ_ticker, date) — open interest is * reported at daily granularity. `source` carries provenance (e.g. the * provider name) the same way `QuoteRow.source` does. */ interface OiDailyRow { occ_ticker: string; underlying: string; date: string; expiration: string; strike: number; right: "call" | "put"; open_interest: number; source?: string | null; } /** * Result of `store.getCoverage(...)`. * * `earliest` / `latest` are ISO date strings (YYYY-MM-DD) or `null` when no data * covers the requested range. `missingDates` is the list of trading dates within * the requested window that have no partition. `totalDates` is the count of * trading dates in the requested window (inclusive on both ends). */ interface CoverageReport { earliest: string | null; latest: string | null; missingDates: string[]; totalDates: number; } /** * Per-leg envelope for QuoteStore.readWindow. Compiled from the strategy's * legs and the entry-window [minSpot, maxSpot] (P2). Strike bands are optional * for legs whose method doesn't constrain strike (e.g. unknown-spot fallback). */ interface LegEnvelope { contractType: "call" | "put"; dteMin: number; dteMax: number; strikeMin?: number; strikeMax?: number; } interface ReadWindowParams { underlying: string; date: string; timeStart: string; timeEnd: string; legEnvelopes: LegEnvelope[]; /** * Opt-in greek projection. Absent ⇒ the full five-greek projection is read * (delta/gamma/theta/vega/iv), byte-identical to the historic behavior. * Present ⇒ the SQL projects only the listed greeks from the partition and * emits NULL for the rest; every returned row is stamped with * `WindowQuoteRow.projectedGreeks` so a null greek that was never requested * is distinguishable from a null greek whose data is genuinely missing. * * Non-greek columns (bid/ask/chain metadata/greeks_source) are always * projected in full regardless of this setting. An unknown greek name throws. * Selection typically needs only `["delta", "iv"]`; gamma/theta/vega feed * downstream reporting snapshots. */ neededGreeks?: ReadonlyArray; } /** * Output row of `QuoteStore.readWindow`. Phase-2 perf: `underlying`, `date`, * and `mid` were removed from the SELECT projection — `underlying` and `date` * are pinned by the call's `ReadWindowParams`, and `mid` is computed downstream * as `(bid + ask) / 2` in `toMinuteQuoteRow`. Skipping these three columns * cuts decode work for the 100K-row hot path on wide-envelope strategies. */ interface WindowQuoteRow { ticker: string; time: string; contract_type: "call" | "put"; strike: number; expiration: string; dte: number; bid: number; ask: number; delta: number | null; gamma: number | null; theta: number | null; vega: number | null; iv: number | null; greeks_source: "massive" | "thetadata" | "computed" | null; /** * The greek subset deliberately projected by this read, echoed back from * `ReadWindowParams.neededGreeks`. Absent when the read used the full * projection (every greek is meaningful). Present ⇒ only the listed greeks * carry meaningful values; the others are NULL because they were not * requested, NOT because their data is missing. Pass this straight to * `hasQuoteGreeks(row, projectedGreeks)` so a trimmed read is never mistaken * for a data-missing read and collapsed to zero candidates. */ projectedGreeks?: ReadonlyArray; } declare abstract class SpotStore { protected readonly ctx: StoreContext; constructor(ctx: StoreContext); /** * Return `read_parquet([...])` SQL over exact `ticker=X/date=Y/data.parquet` * files for a (ticker, from..to) range, or null if no files exist on disk. * Used by concrete stores to bypass the `market.spot` view's glob walk. */ protected buildDirectParquetReadBarsSQL(ticker: string, from: string, to: string, opts?: { rthOnly?: boolean; dailyAgg?: boolean; }): { sql: string; } | null; /** * Public accessor for the data directory root (WR-03). * * Pipeline-side helpers (e.g., `executeFetchPlan`) need the absolute base * directory when no explicit `baseDir` is supplied — the flat-import-log * JSON adapter writes its dedupe ledger under `{dataDir}/market/.flat-import-log/`. * Exposing this through a public getter beats reaching into `store["ctx"]` * via bracket notation, which silently bypasses TypeScript's `protected` * modifier and creates a hidden coupling to the internal field name. */ get dataDir(): string; abstract writeBars(ticker: string, date: string, bars: BarRow[]): Promise; /** * Write bars for a single (ticker, date) partition from a user-supplied SELECT. * * The SELECT must produce columns matching `market.spot` * (ticker, date, time, open, high, low, close, bid, ask). Rows are expected * to belong to the single partition named in `partition` — the caller is * responsible for filtering upstream; mixed partitions are not rejected * but will be written to the named partition's location (Parquet) or the * single table (DuckDB). * * Parquet mode: `COPY (select) TO spot/ticker=X/date=Y/data.parquet` via * the shared staging-table helper. * * DuckDB mode: `INSERT OR REPLACE INTO market.spot (cols...)