use crate::api::types::Ohlc; #[cfg(any(test, feature = "testing"))] use crate::api::types::{Fundamentals, Quote}; use crate::error::IdxError; use super::map::parse_history; #[cfg(any(test, feature = "testing"))] use super::map::{parse_fundamentals, parse_quote}; use super::raw_types::ChartResponse; #[cfg(any(test, feature = "testing"))] use super::raw_types::QuoteSummaryResponse; #[cfg(any(test, feature = "testing"))] pub(crate) fn parse_quote_from_str(symbol: &str, raw: &str) -> Result { let chart: ChartResponse = serde_json::from_str(raw).map_err(|e| IdxError::ParseError(e.to_string()))?; parse_quote(symbol, &chart) } #[cfg(test)] pub(crate) fn parse_history_from_str(symbol: &str, raw: &str) -> Result, IdxError> { parse_history_from_str_with_verbose(symbol, raw, false) } #[cfg(any(test, feature = "testing"))] pub(crate) fn parse_history_from_str_with_verbose( symbol: &str, raw: &str, verbose: bool, ) -> Result, IdxError> { let chart: ChartResponse = serde_json::from_str(raw).map_err(|e| IdxError::ParseError(e.to_string()))?; parse_history_with_verbose(symbol, &chart, verbose) } #[cfg(any(test, feature = "testing"))] pub(crate) fn parse_fundamentals_from_str( symbol: &str, raw: &str, ) -> Result { let quote_summary: QuoteSummaryResponse = serde_json::from_str(raw).map_err(|e| IdxError::ParseError(e.to_string()))?; parse_fundamentals(symbol, "e_summary) } pub(super) fn parse_history_with_verbose( symbol: &str, chart: &ChartResponse, verbose: bool, ) -> Result, IdxError> { let (history, dropped) = parse_history(symbol, chart)?; if dropped > 0 && verbose { crate::runtime::warn(format!( "dropped {dropped} OHLC row(s) from Yahoo response due to missing fields" )); } Ok(history) } #[cfg(test)] mod tests { use super::{ ChartResponse, parse_fundamentals_from_str, parse_history_from_str, parse_history_from_str_with_verbose, parse_history_with_verbose, parse_quote, parse_quote_from_str, }; const SAMPLE: &str = r#"{ "chart": { "result": [{ "meta": { "symbol": "BBCA.JK", "regularMarketPrice": 9875.0, "previousClose": 9758.0, "regularMarketVolume": 12300000, "marketCap": 1215200000000000, "fiftyTwoWeekHigh": 10250.0, "fiftyTwoWeekLow": 7800.0, "averageDailyVolume3Month": 10000000 }, "timestamp": [1709251200,1709337600], "indicators": {"quote":[{ "open":[9800.0,9850.0], "high":[9900.0,9900.0], "low":[9750.0,9800.0], "close":[9875.0,9880.0], "volume":[12300000,11000000] }]} }] } }"#; #[test] fn parses_quote_and_history() { let chart: ChartResponse = serde_json::from_str(SAMPLE).expect("valid chart fixture"); let quote = parse_quote("BBCA.JK", &chart).expect("quote parsed"); assert_eq!(quote.symbol, "BBCA.JK"); assert_eq!(quote.price, 9875); let history = parse_history_with_verbose("BBCA.JK", &chart, false).expect("history parsed"); assert_eq!(history.len(), 2); assert_eq!(history[0].close, 9875); } #[test] fn history_dates_use_exchange_offset_not_utc() { // Weekly bars are stamped at Monday 00:00 WIB, i.e. Sunday 17:00Z. let raw = r#"{"chart":{"error":null,"result":[{ "meta":{"gmtoffset":25200}, "timestamp":[1790528400], "indicators":{"quote":[{"open":[6000],"high":[6150],"low":[5975],"close":[6100],"volume":[1000]}]} }]}}"#; let history = parse_history_from_str("BBCA.JK", raw).expect("history parsed"); assert_eq!(history[0].date.to_string(), "2026-09-28"); let without_offset = raw.replace(r#""gmtoffset":25200"#, r#""symbol":"BBCA.JK""#); let history = parse_history_from_str("BBCA.JK", &without_offset).expect("history parsed"); assert_eq!(history[0].date.to_string(), "2026-09-28"); } #[test] fn parses_realistic_fixture_json() { let quote_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_1d.json").expect("fixture exists"); let history_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_3mo.json").expect("fixture exists"); let fundamentals_raw = std::fs::read_to_string("tests/fixtures/quotesummary_bbca.json") .expect("fixture exists"); let quote = parse_quote_from_str("BBCA.JK", "e_raw).expect("fixture quote parsed"); assert_eq!(quote.symbol, "BBCA.JK"); assert_eq!(quote.market_cap, Some(1_215_200_000_000_000)); assert_eq!(quote.avg_volume, Some(10_000_000)); let history = parse_history_from_str("BBCA.JK", &history_raw).expect("fixture history parsed"); assert!(!history.is_empty()); let verbose_history = parse_history_from_str_with_verbose("BBCA.JK", &history_raw, true) .expect("fixture history parsed in verbose mode"); assert_eq!(verbose_history.len(), history.len()); assert_eq!(verbose_history[0].close, history[0].close); let fundamentals = parse_fundamentals_from_str("BBCA.JK", &fundamentals_raw) .expect("fixture fundamentals parsed"); assert_eq!(fundamentals.trailing_pe, Some(25.4)); assert_eq!(fundamentals.forward_pe, Some(23.1)); assert_eq!(fundamentals.price_to_book, Some(4.6)); assert_eq!(fundamentals.earnings_growth, Some(0.121)); assert_eq!(fundamentals.enterprise_value, Some(1_245_000_000_000_000)); assert_eq!(fundamentals.ebitda, Some(58_500_000_000_000)); assert_eq!(fundamentals.market_cap, Some(1_215_200_000_000_000)); } #[test] fn maps_not_found_chart_error_to_symbol_not_found() { let raw = r#"{"chart":{"result":null,"error":{"code":"Not Found","description":"No data found"}}}"#; let err = parse_quote_from_str("INVALID.JK", raw).expect_err("expected symbol error"); assert!(matches!(err, crate::error::IdxError::SymbolNotFound(_))); } }