From 0a7d123f90afac56f2ad57015216f9d50781748d Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Fri, 6 Mar 2026 11:23:04 +0000 Subject: [PATCH] fix(api): disable unsupported MSN history --- src/api/msn/client.rs | 15 +----------- src/api/msn/mod.rs | 54 ++++++++++++++++++------------------------- src/api/msn/parse.rs | 2 ++ src/error.rs | 7 ++++++ tests/cli.rs | 25 ++++++++++++++++++++ 5 files changed, 57 insertions(+), 46 deletions(-) diff --git a/src/api/msn/client.rs b/src/api/msn/client.rs index 4921749..659170b 100644 --- a/src/api/msn/client.rs +++ b/src/api/msn/client.rs @@ -4,7 +4,7 @@ use serde::de::DeserializeOwned; use crate::error::IdxError; -use super::parse::{KeyRatios, MsnChart, MsnQuote}; +use super::parse::{KeyRatios, MsnQuote}; use super::symbols::resolve_msn_id; const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; @@ -71,17 +71,4 @@ impl MsnClient { format!("{MSN_API_BASE_URL}keyratios?apikey={MSN_API_KEY}&ids={id}&wrapodata=false"); self.get_json(&url, symbol, "keyratios") } - - pub(super) fn fetch_charts( - &self, - symbol: &str, - chart_type: &str, - ) -> Result, IdxError> { - let id = - resolve_msn_id(symbol).ok_or_else(|| IdxError::SymbolNotFound(symbol.to_string()))?; - let url = format!( - "{MSN_ASSETS_BASE_URL}Finance/Charts?apikey={MSN_API_KEY}&cm=id-id&ids={id}&type={chart_type}&wrapodata=false" - ); - self.get_json(&url, symbol, "chart") - } } diff --git a/src/api/msn/mod.rs b/src/api/msn/mod.rs index 6a6fbbf..4be68f4 100644 --- a/src/api/msn/mod.rs +++ b/src/api/msn/mod.rs @@ -7,12 +7,12 @@ use crate::api::types::{Fundamentals, Interval, Ohlc, Period, Quote}; use crate::error::IdxError; use client::MsnClient; -use parse::{ - ResampleInterval, parse_fundamentals, parse_history_with_verbose, parse_quote, resample_history, -}; +use parse::{parse_fundamentals, parse_quote}; pub(crate) use parse::{parse_fundamentals_from_str, parse_history_from_str, parse_quote_from_str}; +const HISTORY_UNSUPPORTED_REASON: &str = "MSN provider does not currently support history or technical analysis because MSN charts do not consistently expose real OHLCV data"; + pub struct MsnProvider { client: MsnClient, verbose: bool, @@ -25,17 +25,6 @@ impl MsnProvider { verbose, } } - - fn chart_type_for_period(period: &Period) -> &'static str { - match period { - Period::OneDay => "1D1M", - Period::FiveDays | Period::OneMonth => "1M", - Period::ThreeMonths => "3M", - Period::SixMonths | Period::OneYear => "1Y", - Period::TwoYears => "3Y", - Period::FiveYears => "5Y", - } - } } impl MarketDataProvider for MsnProvider { @@ -62,32 +51,33 @@ impl MarketDataProvider for MsnProvider { fn history( &self, - symbol: &str, - period: &Period, - interval: &Interval, + _symbol: &str, + _period: &Period, + _interval: &Interval, ) -> Result, IdxError> { - let charts = self - .client - .fetch_charts(symbol, Self::chart_type_for_period(period))?; - let rows = parse_history_with_verbose(period, &charts, self.verbose)?; - Ok(match interval { - Interval::Day => rows, - Interval::Week => resample_history(&rows, ResampleInterval::Week), - Interval::Month => resample_history(&rows, ResampleInterval::Month), - }) + Err(IdxError::Unsupported( + HISTORY_UNSUPPORTED_REASON.to_string(), + )) } } #[cfg(test)] mod tests { use super::MsnProvider; - use crate::api::types::Period; + use crate::api::MarketDataProvider; + use crate::api::types::{Interval, Period}; + use crate::error::IdxError; #[test] - fn maps_periods_to_supported_chart_types() { - assert_eq!(MsnProvider::chart_type_for_period(&Period::OneDay), "1D1M"); - assert_eq!(MsnProvider::chart_type_for_period(&Period::SixMonths), "1Y"); - assert_eq!(MsnProvider::chart_type_for_period(&Period::TwoYears), "3Y"); - assert_eq!(MsnProvider::chart_type_for_period(&Period::FiveYears), "5Y"); + fn history_is_explicitly_unsupported() { + let provider = MsnProvider::new(false); + let err = provider + .history("BBCA.JK", &Period::OneMonth, &Interval::Day) + .expect_err("history should be unsupported"); + assert!(matches!(err, IdxError::Unsupported(_))); + assert!( + err.to_string() + .contains("MSN provider does not currently support history or technical analysis") + ); } } diff --git a/src/api/msn/parse.rs b/src/api/msn/parse.rs index 01e1ed6..b73b44f 100644 --- a/src/api/msn/parse.rs +++ b/src/api/msn/parse.rs @@ -351,12 +351,14 @@ fn sanitize_current_ratio(value: Option) -> Option { }) } +#[allow(dead_code)] #[derive(Clone, Copy)] pub(super) enum ResampleInterval { Week, Month, } +#[allow(dead_code)] pub(super) fn resample_history(rows: &[Ohlc], interval: ResampleInterval) -> Vec { let mut grouped: BTreeMap<(i32, u32), Ohlc> = BTreeMap::new(); diff --git a/src/error.rs b/src/error.rs index 87cdec2..e23c960 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,8 @@ pub enum IdxError { RateLimited, #[error("provider unavailable")] ProviderUnavailable, + #[error("unsupported: {0}")] + Unsupported(String), #[error("parse error: {0}")] ParseError(String), #[error("cache miss: {0}")] @@ -27,6 +29,7 @@ pub enum ErrorCode { SymbolNotFound, RateLimited, ProviderUnavailable, + Unsupported, ParseError, CacheMiss, ConfigError, @@ -40,6 +43,7 @@ impl IdxError { Self::SymbolNotFound(_) => ErrorCode::SymbolNotFound, Self::RateLimited => ErrorCode::RateLimited, Self::ProviderUnavailable => ErrorCode::ProviderUnavailable, + Self::Unsupported(_) => ErrorCode::Unsupported, Self::ParseError(_) => ErrorCode::ParseError, Self::CacheMiss(_) => ErrorCode::CacheMiss, Self::ConfigError(_) => ErrorCode::ConfigError, @@ -65,5 +69,8 @@ mod tests { let parse = IdxError::ParseError("bad json".to_string()); assert_eq!(parse.code(), ErrorCode::ParseError); + + let unsupported = IdxError::Unsupported("history unavailable".to_string()); + assert_eq!(unsupported.code(), ErrorCode::Unsupported); } } diff --git a/tests/cli.rs b/tests/cli.rs index 4c605b3..0e9d063 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -106,6 +106,31 @@ fn technical_with_mock_provider_json_contains_fields() { .stdout(predicate::str::contains("\"signals\"")); } +#[test] +fn msn_history_reports_unsupported() { + test_bin("msn-history-unsupported") + .env("IDX_PROVIDER", "msn") + .args(["stocks", "history", "BBCA", "--period", "1mo"]) + .assert() + .failure() + .stderr(predicate::str::contains( + "MSN provider does not currently support history or technical analysis", + )); +} + +#[test] +fn msn_technical_json_reports_unsupported() { + test_bin("msn-technical-unsupported") + .env("IDX_PROVIDER", "msn") + .args(["-o", "json", "stocks", "technical", "BBCA"]) + .assert() + .failure() + .stderr(predicate::str::contains("\"code\": \"UNSUPPORTED\"")) + .stderr(predicate::str::contains( + "MSN provider does not currently support history or technical analysis", + )); +} + #[test] fn growth_with_mock_provider_table_contains_expected_rows() { test_bin("growth-table")