pub mod bandar; pub mod fundamental; pub mod report; pub mod signals; pub mod technical; use serde::Serialize; use serde::de::DeserializeOwned; use crate::api::history_provider; use crate::api::types::{Fundamentals, Interval, Period}; use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange}; use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch}; use crate::config::HistoryProviderKind; use crate::error::IdxError; use crate::market::HistorySource; use bandar::{BandarReport, analyze_bandar, check_top}; use fundamental::{ FundamentalReport, GrowthReport, RiskReport, ValuationReport, analyze_fundamental, analyze_growth, analyze_risk, analyze_valuation, }; use report::{TechnicalReport, build_technical_report}; /// Analysis reports. Reports are cached as computed, not their inputs. pub struct Analysis<'a> { pub(crate) idx: &'a Idx, } /// Comparison results; symbols that failed are listed with their error. pub struct Comparison { pub reports: Vec>, pub failures: Vec<(String, IdxError)>, } /// One investor side is weighed against its own selling, which the source /// gives only across all boards. fn check_scope(investor: BrokerInvestor, board: BrokerBoard) -> Result<(), IdxError> { if investor == BrokerInvestor::All || board == BrokerBoard::All { Ok(()) } else { Err(IdxError::InvalidInput(format!( "bandar for {} investors needs --board all: the broker summary gives one side's \ selling only across all boards", investor.as_str() ))) } } /// Bandar readings for several symbols, strongest accumulation first; /// symbols that failed are listed with their error. pub struct BandarScreen { pub reports: Vec>, pub failures: Vec<(String, IdxError)>, } /// The sessions and trades a bandar reading covers. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BandarQuery { pub range: BrokerRange, pub investor: BrokerInvestor, pub board: BrokerBoard, /// Headline tier, one of `bandar::TIERS`. pub top: u8, } impl Analysis<'_> { /// Bandarmology over the broker summary `query` selects. pub fn bandar( &self, symbol: &str, query: &BandarQuery, policy: CachePolicy, ) -> Result, IdxError> { let top = check_top(query.top)?; check_scope(query.investor, query.board)?; let summary = self.idx .broker() .fetch(symbol, query.range, query.investor, query.board, policy)?; Ok(summary.map(|summary| analyze_bandar(&summary, top))) } /// [`Self::bandar`] for each symbol, ranked by the headline imbalance. /// Invalid input fails the whole screen; other errors fail one symbol. pub fn bandar_screen>( &self, symbols: &[S], query: &BandarQuery, policy: CachePolicy, ) -> Result { check_top(query.top)?; check_scope(query.investor, query.board)?; let mut reports = Vec::new(); let mut failures = Vec::new(); for symbol in symbols { let resolved = self.idx.resolve(symbol.as_ref())?; match self.bandar(&resolved, query, policy) { Ok(report) => reports.push(report), Err(err @ IdxError::InvalidInput(_)) => return Err(err), Err(err) => failures.push((resolved, err)), } } if reports.is_empty() { return Err(failures .pop() .map(|(_, err)| err) .unwrap_or_else(|| IdxError::InvalidInput("no symbols to screen".to_string()))); } // Strongest accumulation first; readings without an imbalance last. reports.sort_by_key(|report| std::cmp::Reverse(report.data.imbalance_bps)); Ok(BandarScreen { reports, failures }) } /// Where `technical` reads its year of daily history from, for an /// optional `--history-provider` override. pub fn technical_source(&self, mode: Option) -> HistorySource { self.idx .market() .history_source(mode, &Period::OneYear, &Interval::Day) } /// Technical analysis over one year of daily history from `source` /// (see `technical_source`). pub fn technical( &self, symbol: &str, source: HistorySource, policy: CachePolicy, ) -> Result, IdxError> { let idx = self.idx; let resolved = idx.resolve(symbol)?; idx.market().with_history_fallback(source, |source| { let provider = history_provider(source.provider, idx.config.verbose); let bucket = format!("{}-technical", source.provider.as_str()); cached_fetch( &idx.cache, CacheSpec { bucket: &bucket, key: &resolved, ttl_secs: idx.config.quote_ttl, subject: &resolved, source: source.provider.as_str(), }, policy, || provider.history(&resolved, &Period::OneYear, &Interval::Day), |history| build_technical_report(&resolved, &history), ) }) } pub fn growth( &self, symbol: &str, policy: CachePolicy, ) -> Result, IdxError> { self.fundamental_report(symbol, "growth", policy, |_, f| analyze_growth(f)) } pub fn valuation( &self, symbol: &str, policy: CachePolicy, ) -> Result, IdxError> { self.fundamental_report(symbol, "valuation", policy, |_, f| analyze_valuation(f)) } pub fn risk(&self, symbol: &str, policy: CachePolicy) -> Result, IdxError> { self.fundamental_report(symbol, "risk", policy, |_, f| analyze_risk(f)) } pub fn fundamental( &self, symbol: &str, policy: CachePolicy, ) -> Result, IdxError> { self.fundamental_report(symbol, "fundamental", policy, analyze_fundamental) } /// Fundamental reports for each symbol. Fails only if every symbol fails /// (with the last error) or a symbol cannot be resolved. pub fn compare>( &self, symbols: &[S], policy: CachePolicy, ) -> Result { let mut reports = Vec::new(); let mut failures = Vec::new(); for symbol in symbols { let resolved = self.idx.resolve(symbol.as_ref())?; match self.fundamental(&resolved, policy) { Ok(report) => reports.push(report), Err(err) => failures.push((resolved, err)), } } if reports.is_empty() { return Err(failures.pop().map(|(_, err)| err).unwrap_or_else(|| { IdxError::CacheMiss("fundamental/no symbols could be compared".to_string()) })); } Ok(Comparison { reports, failures }) } fn fundamental_report( &self, symbol: &str, bucket: &str, policy: CachePolicy, analyzer: impl FnOnce(&str, &Fundamentals) -> T, ) -> Result, IdxError> where T: Serialize + DeserializeOwned, { let idx = self.idx; let resolved = idx.resolve(symbol)?; let bucket = idx.bucket(bucket); cached_fetch( &idx.cache, CacheSpec { bucket: &bucket, key: &resolved, ttl_secs: idx.config.fundamental_ttl, subject: &resolved, source: idx.provider.kind().as_str(), }, policy, || idx.provider.market().fundamentals(&resolved), |fundamentals| Ok(analyzer(&resolved, &fundamentals)), ) } }