//! Fixture-backed provider for tests, enabled by the `testing` feature and //! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it. use super::types::{Bar, Fundamentals, Interval, Period, Quote}; use super::{ BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, QuoteProvider, TradingSummaryProvider, idx, ipot, msn, yahoo, }; use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery}; use crate::config::ProviderKind; use crate::error::IdxError; pub(crate) fn enabled() -> bool { std::env::var("IDX_USE_MOCK_PROVIDER").is_ok() } /// `IDX_MOCK_TODAY` (YYYY-MM-DD) pins today's date under the mock provider. pub(crate) fn today() -> Option { if !enabled() { return None; } let raw = std::env::var("IDX_MOCK_TODAY").ok()?; chrono::NaiveDate::parse_from_str(&raw, "%Y-%m-%d").ok() } pub struct MockProvider { quote: Result, fundamentals: Result, history: Result, IdxError>, } impl MockProvider { pub fn from_fixtures(provider: ProviderKind) -> Self { Self::from_fixtures_with_history_verbose(provider, false) } pub fn from_fixtures_with_history_verbose( provider: ProviderKind, history_verbose: bool, ) -> Self { if std::env::var("IDX_MOCK_ERROR").is_ok() { return Self::with_error(IdxError::ProviderUnavailable); } match provider { ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose), ProviderKind::Msn => Self::from_msn_fixtures(), } } fn from_yahoo_fixtures(history_verbose: bool) -> Self { let quote_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_1d.json") .unwrap_or_else(|_| "{}".to_string()); let history_path = std::env::var("IDX_MOCK_YAHOO_HISTORY_FIXTURE") .unwrap_or_else(|_| "tests/fixtures/chart_bbca_3mo.json".to_string()); let history_raw = std::fs::read_to_string(&history_path).unwrap_or_else(|_| "{}".to_string()); let fundamentals_raw = std::fs::read_to_string("tests/fixtures/quotesummary_bbca.json") .unwrap_or_else(|_| "{}".to_string()); let quote = yahoo::parse_quote_from_str("BBCA.JK", "e_raw) .map_err(|e| IdxError::ParseError(e.to_string())); let fundamentals = yahoo::parse_fundamentals_from_str("BBCA.JK", &fundamentals_raw) .map_err(|e| IdxError::ParseError(e.to_string())); let history = yahoo::parse_history_from_str_with_verbose("BBCA.JK", &history_raw, history_verbose) .map_err(|e| IdxError::ParseError(e.to_string())); Self { quote, fundamentals, history, } } fn from_msn_fixtures() -> Self { let quote_raw = std::fs::read_to_string("tests/fixtures/msn_quote_bbca.json") .unwrap_or_else(|_| "[]".to_string()); let fundamentals_path = std::env::var("IDX_MOCK_MSN_KEYRATIOS_FIXTURE") .unwrap_or_else(|_| "tests/fixtures/msn_keyratios_bbca.json".to_string()); let fundamentals_raw = std::fs::read_to_string(&fundamentals_path).unwrap_or_else(|_| "[]".to_string()); let quote = msn::parse_quote_from_str("BBCA.JK", "e_raw) .map_err(|e| IdxError::ParseError(e.to_string())); let fundamentals = msn::parse_fundamentals_from_str(&fundamentals_raw, Some("e_raw)) .map_err(|e| IdxError::ParseError(e.to_string())); let history_raw = std::fs::read_to_string("tests/fixtures/msn_chart_bbca_3m.json") .unwrap_or_else(|_| "[]".to_string()); let history = msn::parse_history_from_str("BBCA.JK", &history_raw) .map_err(|e| IdxError::ParseError(e.to_string())); Self { quote, fundamentals, history, } } pub fn with_error(err: IdxError) -> Self { Self { quote: Err(err.clone()), fundamentals: Err(err.clone()), history: Err(err), } } } impl QuoteProvider for MockProvider { fn quote(&self, symbol: &str) -> Result { let mut q = self.quote.clone()?; q.symbol = symbol.to_string(); Ok(q) } } impl FundamentalsProvider for MockProvider { fn fundamentals(&self, _symbol: &str) -> Result { self.fundamentals.clone() } } impl HistoryProvider for MockProvider { fn history( &self, _symbol: &str, _period: &Period, _interval: &Interval, ) -> Result, IdxError> { self.history.clone() } } /// Broker summaries from `tests/fixtures/ipot_brokersummary__ /// [_][__].html`; a query without a fixture reads as an /// unknown code. pub struct MockBrokerProvider; impl BrokerSummaryProvider for MockBrokerProvider { fn broker_summary( &self, code: &str, query: &BrokerQuery, ) -> Result { if std::env::var("IDX_MOCK_ERROR").is_ok() { return Err(IdxError::ProviderUnavailable); } let mut name = format!( "ipot_brokersummary_{}_{}", code.to_ascii_lowercase(), query.from.format("%Y%m%d") ); if query.to != query.from { name.push_str(&query.to.format("_%Y%m%d").to_string()); } if (query.investor, query.board) != (BrokerInvestor::All, BrokerBoard::All) { name.push_str(&format!( "_{}_{}", query.investor.as_str(), query.board.as_str() )); } let html = std::fs::read_to_string(format!("tests/fixtures/{name}.html")) .unwrap_or_else(|_| "Invalid stock code!".to_string()); ipot::summary_from_html(code, &html) } } /// Trading summaries from `tests/fixtures/idx_stocksummary_.json`; a /// date without a fixture had no trading. pub struct MockTradingSummaryProvider; impl TradingSummaryProvider for MockTradingSummaryProvider { fn stock_summary( &self, date: chrono::NaiveDate, ) -> Result, IdxError> { if std::env::var("IDX_MOCK_ERROR").is_ok() { return Err(IdxError::ProviderUnavailable); } let path = format!( "tests/fixtures/idx_stocksummary_{}.json", date.format("%Y%m%d") ); match std::fs::read_to_string(path) { Ok(raw) => idx::parse_stock_summary(&raw, date), Err(_) => Ok(Vec::new()), } } }