diff --git a/src/api/msn/map.rs b/src/api/msn/map.rs index 4e3a8fc..bb3581a 100644 --- a/src/api/msn/map.rs +++ b/src/api/msn/map.rs @@ -460,9 +460,15 @@ pub(super) fn parse_sentiment( symbol: &str, raw: &[RawSentiment], ) -> Result { - let item = raw - .first() - .ok_or_else(|| IdxError::ParseError("no sentiment data".into()))?; + // MSN answers `[]` for tickers nobody has voted on (all IDX names as of + // 2026-10). That is "no votes", not a malformed response. + let Some(item) = raw.first() else { + let ticker = ticker_from_symbol(symbol).unwrap_or_default(); + return Ok(SentimentData { + symbol: normalized_symbol(symbol, &ticker), + statistics: Vec::new(), + }); + }; let stats = item .sentiment_statistics .as_ref() @@ -929,6 +935,13 @@ mod tests { ); } + #[test] + fn empty_sentiment_response_is_empty_data_not_an_error() { + let sentiment = parse_sentiment("BBCA", &[]).expect("empty sentiment is valid"); + assert_eq!(sentiment.symbol, "BBCA.JK"); + assert!(sentiment.statistics.is_empty()); + } + #[test] fn parses_msn_chart_price_only_fixture_as_synthetic_ohlc() { let raw: Vec = serde_json::from_str(include_str!( diff --git a/src/output/table.rs b/src/output/table.rs index 1b92e1b..429f92d 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -768,6 +768,10 @@ pub fn print_earnings(report: &EarningsReport) -> Result<(), IdxError> { } pub fn print_sentiment(data: &SentimentData) -> Result<(), IdxError> { + if data.statistics.is_empty() { + println!("No sentiment votes for {}.", data.symbol); + return Ok(()); + } let mut table = Table::new(); table .load_preset(UTF8_FULL)