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

@ -245,6 +245,18 @@ fn msn_capability_error(subject: &str) -> IdxError {
IdxError::Unsupported(format!("{subject}: command requires --provider msn"))
}
/// The provider that serves history for `provider` under `history_mode`.
pub fn history_source(provider: ProviderKind, history_mode: HistoryProviderKind) -> ProviderKind {
match history_mode {
HistoryProviderKind::Yahoo => ProviderKind::Yahoo,
HistoryProviderKind::Msn => ProviderKind::Msn,
HistoryProviderKind::Auto => match provider {
ProviderKind::Yahoo => ProviderKind::Yahoo,
ProviderKind::Msn => ProviderKind::Yahoo,
},
}
}
/// Resolves a history provider based on the selected market data provider and
/// history provider strategy.
///
@ -255,14 +267,7 @@ pub fn history_provider(
history_mode: HistoryProviderKind,
verbose: bool,
) -> Result<(ProviderKind, Box<dyn HistoryProvider>), IdxError> {
let resolved = match history_mode {
HistoryProviderKind::Yahoo => ProviderKind::Yahoo,
HistoryProviderKind::Msn => ProviderKind::Msn,
HistoryProviderKind::Auto => match provider {
ProviderKind::Yahoo => ProviderKind::Yahoo,
ProviderKind::Msn => ProviderKind::Yahoo,
},
};
let resolved = history_source(provider, history_mode);
if mock_enabled()
&& let Some(mock) = mock_history(resolved, verbose)