refactor(core): move stocks fetching, caching, and analysis into idx-core

Phase 3b of docs/DESIGN.md. `stocks` handlers are now argument parsing,
one idx-core call, and rendering.

- Idx (client.rs) is the entry point: Idx::new(CoreConfig) with
  market(), financial(), and analysis(). Live lookups return
  Fetched<T> { data, source, fetched_at, stale } and take a CachePolicy
  (Default | Offline | Refresh, from the --offline/--no-cache flags).
- One cached_fetch replaces the CLI's three cache flows; they differed
  only in whether a fresh entry is read via get or get_offline when
  offline, which yields the same data and diagnostics. Report-building
  errors (technical) still skip the stale-cache fallback.
- market.rs: quotes, history (history_source is resolved first so the
  CLI's fallback note keeps its position), and a typed screener
  (ScreenFilter/ScreenRegion with the same error messages).
- financial.rs: profile, statements, earnings, sentiment, insights,
  news, with StatementFilter/EarningsFilter.
- analysis: TechnicalReport moves to analysis::report; Analysis offers
  technical, growth, valuation, risk, fundamental, and compare.
- CoreConfig::load() owns provider/exchange/TTL settings from
  config.toml and IDX_*; the CLI's IdxConfig adds output and color.
  With two different invalid settings at once, the reported one may
  differ from before; every single-error message is unchanged.
- Cache gains fresh/any/offline accessors returning fetch metadata.

Unit tests moved with their code (core 157 -> 168, CLI 17 -> 7). All
143 goldens unchanged; live A/B against main: 16/16 stocks identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 09:41:31 +00:00
commit 86cb1b4b7c
16 changed files with 1613 additions and 1301 deletions

View file

@ -59,21 +59,25 @@ A single entry point holds configuration, providers, cache, and the DB path.
Domain methods hang off it.
```rust
let idx = Idx::builder()
.config(CoreConfig::load()?) // provider, TTLs, paths; no output settings
.build()?;
let idx = Idx::new(CoreConfig::load()?)?; // provider, TTLs, cache dir; no output settings
let policy = CachePolicy::Default;
// market
let quotes: Fetched<Vec<Quote>> = idx.market().quotes(&["BBCA", "BBRI"], CachePolicy::Default)?;
let candles: Fetched<Vec<Ohlc>> = idx.market().history("BBCA", Period::M3, Interval::D1, CachePolicy::Default)?;
let screen: Fetched<Vec<Quote>> = idx.market().screen(ScreenQuery { filter, region, limit })?;
let quotes: Vec<Fetched<Quote>> = idx.market().quotes(&["BBCA", "BBRI"], policy)?;
let source = idx.market().history_source(None); // which provider serves history
let candles: Fetched<Vec<Ohlc>> =
idx.market().history("BBCA", &Period::ThreeMonths, &Interval::Day, source, policy)?;
let screen: Fetched<Vec<Quote>> = idx.market().screen(&ScreenQuery { filter, region, limit }, policy)?;
// financial
let report: Fetched<FinancialStatements> = idx.financial().statements("BBCA", StatementFilter::all())?;
let earnings: Fetched<EarningsReport> = idx.financial().earnings("BBCA", EarningsFilter::default())?;
let report: Fetched<FinancialStatements> =
idx.financial().statements("BBCA", &StatementFilter::all(), policy)?;
let earnings: Fetched<EarningsReport> =
idx.financial().earnings("BBCA", EarningsFilter::default(), policy)?;
// analysis (fetches its own inputs through the same cache)
let tech: Fetched<TechnicalReport> = idx.analysis().technical("BBCA")?;
let tech: Fetched<TechnicalReport> = idx.analysis().technical("BBCA", source, policy)?;
let compared: Comparison = idx.analysis().compare(&["BBCA", "BBRI"], policy)?;
// investor (local DB)
let holders: TickerOwnership = idx.investor().ticker("BBCA")?;
@ -86,14 +90,18 @@ Key decisions:
- **Blocking API.** `ureq` and `rusqlite` are blocking, and so is the CLI.
Async frontends (MCP, HTTP) call core through `spawn_blocking`. Revisit only
if an HTTP API needs high concurrency.
- **`Fetched<T>` envelope** for live data: `{ data, source, fetched_at, stale }`.
This replaces today's stderr-only stale-cache warning, which an agent or API
client cannot see.
- **`Fetched<T>` envelope** for live data: `{ data, source, fetched_at, stale }`,
so agents and API clients can see freshness. idx-core still emits the
stale-cache warnings through `runtime` (which `set_quiet` silences) so CLI
stderr stays identical.
- **`CachePolicy`**: `Default | Offline | Refresh` replaces the `--offline` /
`--no-cache` flag handling now spread through `cli/stocks.rs`.
- **Explicit configuration.** Core reads no environment variables. `IDX_*` env
parsing, `config.toml` loading, and XDG resolution move to a `CoreConfig::load()`
helper that frontends call; tests construct `CoreConfig` directly.
- **Explicit configuration.** `CoreConfig::load()` is the one place idx-core reads
provider, exchange, and TTL settings from `config.toml` and `IDX_*`; other
frontends and tests construct `CoreConfig` directly (`cache_dir` overrides the
XDG default). Remaining environment hooks: XDG paths for the config file and
ownership DB, `IDX_CURL_IMPERSONATE_BIN`, the IDX discovery URL overrides, and
the `testing`-only mock variables.
- **Mock provider behind a `testing` feature.** Done: `MockProvider` and the
embedded MSN fixtures compile only with idx-core's `testing` feature, which
idx-cli enables as a dev-dependency.