mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
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:
parent
e78d9fcecd
commit
86cb1b4b7c
16 changed files with 1613 additions and 1301 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -9,125 +9,69 @@ use crate::cli::Cli;
|
|||
use crate::output::{OutputFormat, OutputSetting};
|
||||
use idx_core::error::IdxError;
|
||||
|
||||
/// CLI settings: core data-access settings plus presentation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IdxConfig {
|
||||
pub provider: ProviderKind,
|
||||
pub history_provider: HistoryProviderKind,
|
||||
pub exchange: String,
|
||||
pub core: CoreConfig,
|
||||
pub output: OutputFormat,
|
||||
pub no_color: bool,
|
||||
pub quote_ttl: u64,
|
||||
pub fundamental_ttl: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct FileConfig {
|
||||
general: Option<FileGeneral>,
|
||||
cache: Option<FileCache>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct FileGeneral {
|
||||
provider: Option<ProviderKind>,
|
||||
history_provider: Option<HistoryProviderKind>,
|
||||
exchange: Option<String>,
|
||||
output: Option<OutputSetting>,
|
||||
color: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct FileCache {
|
||||
quote_ttl: Option<u64>,
|
||||
fundamental_ttl: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for IdxConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: ProviderKind::Msn,
|
||||
history_provider: HistoryProviderKind::Auto,
|
||||
exchange: "JK".to_string(),
|
||||
output: OutputSetting::Auto.resolve(stdout_is_terminal()),
|
||||
no_color: false,
|
||||
quote_ttl: 300,
|
||||
fundamental_ttl: 3600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IdxConfig {
|
||||
pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> {
|
||||
let mut cfg = Self::load()?;
|
||||
let mut core = CoreConfig::load()?;
|
||||
core.verbose = cli.verbose > 0;
|
||||
let mut output = OutputSetting::Auto.resolve(stdout_is_terminal());
|
||||
let mut no_color = false;
|
||||
|
||||
if let Ok(provider) = std::env::var("IDX_PROVIDER") {
|
||||
cfg.provider = ProviderKind::parse(&provider)?;
|
||||
}
|
||||
if let Ok(history_provider) = std::env::var("IDX_HISTORY_PROVIDER") {
|
||||
cfg.history_provider = HistoryProviderKind::parse(&history_provider)?;
|
||||
}
|
||||
if let Ok(exchange) = std::env::var("IDX_EXCHANGE") {
|
||||
cfg.exchange = exchange;
|
||||
}
|
||||
if let Ok(output) = std::env::var("IDX_OUTPUT") {
|
||||
let setting = OutputSetting::parse(&output).ok_or_else(|| {
|
||||
IdxError::ConfigError(format!(
|
||||
"invalid IDX_OUTPUT value: '{output}', expected 'auto', 'table', 'json', or 'jsonl'"
|
||||
))
|
||||
})?;
|
||||
cfg.output = setting.resolve(stdout_is_terminal());
|
||||
}
|
||||
if let Ok(no_color) = std::env::var("IDX_NO_COLOR") {
|
||||
cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true");
|
||||
}
|
||||
if let Some(parsed) = parse_env_ttl("IDX_CACHE_QUOTE_TTL")? {
|
||||
cfg.quote_ttl = parsed;
|
||||
}
|
||||
if let Some(parsed) = parse_env_ttl("IDX_CACHE_FUNDAMENTAL_TTL")? {
|
||||
cfg.fundamental_ttl = parsed;
|
||||
}
|
||||
|
||||
if let Some(output) = cli.output {
|
||||
cfg.output = output.resolve(stdout_is_terminal());
|
||||
}
|
||||
cfg.no_color = cfg.no_color || cli.no_color || !stdout_is_terminal();
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self, IdxError> {
|
||||
let mut cfg = Self::default();
|
||||
let path = config_path()?;
|
||||
if path.exists() {
|
||||
let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?;
|
||||
let parsed: FileConfig =
|
||||
toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
|
||||
if let Some(general) = parsed.general {
|
||||
if let Some(provider) = general.provider {
|
||||
cfg.provider = provider;
|
||||
}
|
||||
if let Some(history_provider) = general.history_provider {
|
||||
cfg.history_provider = history_provider;
|
||||
}
|
||||
if let Some(exchange) = general.exchange {
|
||||
cfg.exchange = exchange;
|
||||
}
|
||||
if let Some(output) = general.output {
|
||||
cfg.output = output.resolve(stdout_is_terminal());
|
||||
if let Some(setting) = general.output {
|
||||
output = setting.resolve(stdout_is_terminal());
|
||||
}
|
||||
if let Some(color) = general.color {
|
||||
cfg.no_color = !color;
|
||||
}
|
||||
}
|
||||
if let Some(cache) = parsed.cache {
|
||||
if let Some(v) = cache.quote_ttl {
|
||||
cfg.quote_ttl = v;
|
||||
}
|
||||
if let Some(v) = cache.fundamental_ttl {
|
||||
cfg.fundamental_ttl = v;
|
||||
no_color = !color;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(cfg)
|
||||
|
||||
if let Ok(value) = std::env::var("IDX_OUTPUT") {
|
||||
let setting = OutputSetting::parse(&value).ok_or_else(|| {
|
||||
IdxError::ConfigError(format!(
|
||||
"invalid IDX_OUTPUT value: '{value}', expected 'auto', 'table', 'json', or 'jsonl'"
|
||||
))
|
||||
})?;
|
||||
output = setting.resolve(stdout_is_terminal());
|
||||
}
|
||||
if let Ok(value) = std::env::var("IDX_NO_COLOR") {
|
||||
no_color = value == "1" || value.eq_ignore_ascii_case("true");
|
||||
}
|
||||
|
||||
if let Some(setting) = cli.output {
|
||||
output = setting.resolve(stdout_is_terminal());
|
||||
}
|
||||
no_color = no_color || cli.no_color || !stdout_is_terminal();
|
||||
|
||||
Ok(Self {
|
||||
core,
|
||||
output,
|
||||
no_color,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,20 +79,6 @@ pub fn stdout_is_terminal() -> bool {
|
|||
std::io::stdout().is_terminal()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::IdxConfig;
|
||||
|
||||
#[test]
|
||||
fn default_values_are_sane() {
|
||||
let cfg = IdxConfig::default();
|
||||
assert_eq!(cfg.provider, super::ProviderKind::Msn);
|
||||
assert_eq!(cfg.history_provider, super::HistoryProviderKind::Auto);
|
||||
assert_eq!(cfg.exchange, "JK");
|
||||
assert_eq!(cfg.quote_ttl, 300);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod output_values_tests {
|
||||
use clap::ValueEnum;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use clap_complete::{generate, shells};
|
|||
use crate::cli::{Cli, Commands};
|
||||
use crate::config::IdxConfig;
|
||||
use crate::output::{OutputFormat, OutputSetting, emit_error};
|
||||
use idx_core::api::default_provider;
|
||||
use idx_core::error::IdxError;
|
||||
|
||||
fn main() {
|
||||
|
|
@ -53,15 +52,7 @@ fn run() -> Result<(), IdxError> {
|
|||
}
|
||||
}
|
||||
Commands::Stocks(stocks) => {
|
||||
let provider = default_provider(config.provider, cli.verbose > 0);
|
||||
if let Err(err) = cli::stocks::handle(
|
||||
stocks,
|
||||
&config,
|
||||
&provider,
|
||||
cli.offline,
|
||||
cli.no_cache,
|
||||
cli.verbose > 0,
|
||||
) {
|
||||
if let Err(err) = cli::stocks::handle(stocks, &config, cli.offline, cli.no_cache) {
|
||||
emit_error(&err, &config.output);
|
||||
return Err(err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ pub mod table;
|
|||
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use clap::ValueEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
|
||||
use idx_core::analysis::fundamental::{
|
||||
FundamentalReport, GrowthReport, RiskReport, ValuationReport,
|
||||
};
|
||||
use idx_core::analysis::signals::TechnicalSignal;
|
||||
use idx_core::analysis::report::TechnicalReport;
|
||||
use idx_core::api::types::{
|
||||
CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote,
|
||||
SentimentData,
|
||||
|
|
@ -62,34 +61,6 @@ impl OutputFormat {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TechnicalReport {
|
||||
pub symbol: String,
|
||||
pub as_of: NaiveDate,
|
||||
pub current_price: i64,
|
||||
pub sma20: Option<f64>,
|
||||
pub sma50: Option<f64>,
|
||||
pub sma200: Option<f64>,
|
||||
pub rsi14: Option<f64>,
|
||||
pub macd: MacdSnapshot,
|
||||
pub volume: VolumeSnapshot,
|
||||
pub signals: TechnicalSignal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MacdSnapshot {
|
||||
pub line: Option<f64>,
|
||||
pub signal: Option<f64>,
|
||||
pub histogram: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VolumeSnapshot {
|
||||
pub current: u64,
|
||||
pub average20: Option<f64>,
|
||||
pub ratio20: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn render_quotes(
|
||||
quotes: &[Quote],
|
||||
format: &OutputFormat,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::output::style::Paint;
|
||||
use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
|
||||
|
||||
use crate::output::TechnicalReport;
|
||||
use idx_core::analysis::fundamental::{
|
||||
FundamentalReport, GrowthReport, RiskReport, ValuationReport,
|
||||
};
|
||||
use idx_core::analysis::report::TechnicalReport;
|
||||
use idx_core::analysis::signals::Signal;
|
||||
use idx_core::api::types::{
|
||||
CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue