mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-08-07 01:33:52 +00:00
fix(api): disable unsupported MSN history
This commit is contained in:
parent
9b2e32e0c6
commit
0a7d123f90
5 changed files with 57 additions and 46 deletions
|
|
@ -4,7 +4,7 @@ use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use crate::error::IdxError;
|
use crate::error::IdxError;
|
||||||
|
|
||||||
use super::parse::{KeyRatios, MsnChart, MsnQuote};
|
use super::parse::{KeyRatios, MsnQuote};
|
||||||
use super::symbols::resolve_msn_id;
|
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";
|
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");
|
format!("{MSN_API_BASE_URL}keyratios?apikey={MSN_API_KEY}&ids={id}&wrapodata=false");
|
||||||
self.get_json(&url, symbol, "keyratios")
|
self.get_json(&url, symbol, "keyratios")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn fetch_charts(
|
|
||||||
&self,
|
|
||||||
symbol: &str,
|
|
||||||
chart_type: &str,
|
|
||||||
) -> Result<Vec<MsnChart>, 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ use crate::api::types::{Fundamentals, Interval, Ohlc, Period, Quote};
|
||||||
use crate::error::IdxError;
|
use crate::error::IdxError;
|
||||||
|
|
||||||
use client::MsnClient;
|
use client::MsnClient;
|
||||||
use parse::{
|
use parse::{parse_fundamentals, parse_quote};
|
||||||
ResampleInterval, parse_fundamentals, parse_history_with_verbose, parse_quote, resample_history,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) use parse::{parse_fundamentals_from_str, parse_history_from_str, parse_quote_from_str};
|
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 {
|
pub struct MsnProvider {
|
||||||
client: MsnClient,
|
client: MsnClient,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
|
@ -25,17 +25,6 @@ impl MsnProvider {
|
||||||
verbose,
|
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 {
|
impl MarketDataProvider for MsnProvider {
|
||||||
|
|
@ -62,32 +51,33 @@ impl MarketDataProvider for MsnProvider {
|
||||||
|
|
||||||
fn history(
|
fn history(
|
||||||
&self,
|
&self,
|
||||||
symbol: &str,
|
_symbol: &str,
|
||||||
period: &Period,
|
_period: &Period,
|
||||||
interval: &Interval,
|
_interval: &Interval,
|
||||||
) -> Result<Vec<Ohlc>, IdxError> {
|
) -> Result<Vec<Ohlc>, IdxError> {
|
||||||
let charts = self
|
Err(IdxError::Unsupported(
|
||||||
.client
|
HISTORY_UNSUPPORTED_REASON.to_string(),
|
||||||
.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),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::MsnProvider;
|
use super::MsnProvider;
|
||||||
use crate::api::types::Period;
|
use crate::api::MarketDataProvider;
|
||||||
|
use crate::api::types::{Interval, Period};
|
||||||
|
use crate::error::IdxError;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_periods_to_supported_chart_types() {
|
fn history_is_explicitly_unsupported() {
|
||||||
assert_eq!(MsnProvider::chart_type_for_period(&Period::OneDay), "1D1M");
|
let provider = MsnProvider::new(false);
|
||||||
assert_eq!(MsnProvider::chart_type_for_period(&Period::SixMonths), "1Y");
|
let err = provider
|
||||||
assert_eq!(MsnProvider::chart_type_for_period(&Period::TwoYears), "3Y");
|
.history("BBCA.JK", &Period::OneMonth, &Interval::Day)
|
||||||
assert_eq!(MsnProvider::chart_type_for_period(&Period::FiveYears), "5Y");
|
.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")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -351,12 +351,14 @@ fn sanitize_current_ratio(value: Option<f64>) -> Option<f64> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(super) enum ResampleInterval {
|
pub(super) enum ResampleInterval {
|
||||||
Week,
|
Week,
|
||||||
Month,
|
Month,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(super) fn resample_history(rows: &[Ohlc], interval: ResampleInterval) -> Vec<Ohlc> {
|
pub(super) fn resample_history(rows: &[Ohlc], interval: ResampleInterval) -> Vec<Ohlc> {
|
||||||
let mut grouped: BTreeMap<(i32, u32), Ohlc> = BTreeMap::new();
|
let mut grouped: BTreeMap<(i32, u32), Ohlc> = BTreeMap::new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ pub enum IdxError {
|
||||||
RateLimited,
|
RateLimited,
|
||||||
#[error("provider unavailable")]
|
#[error("provider unavailable")]
|
||||||
ProviderUnavailable,
|
ProviderUnavailable,
|
||||||
|
#[error("unsupported: {0}")]
|
||||||
|
Unsupported(String),
|
||||||
#[error("parse error: {0}")]
|
#[error("parse error: {0}")]
|
||||||
ParseError(String),
|
ParseError(String),
|
||||||
#[error("cache miss: {0}")]
|
#[error("cache miss: {0}")]
|
||||||
|
|
@ -27,6 +29,7 @@ pub enum ErrorCode {
|
||||||
SymbolNotFound,
|
SymbolNotFound,
|
||||||
RateLimited,
|
RateLimited,
|
||||||
ProviderUnavailable,
|
ProviderUnavailable,
|
||||||
|
Unsupported,
|
||||||
ParseError,
|
ParseError,
|
||||||
CacheMiss,
|
CacheMiss,
|
||||||
ConfigError,
|
ConfigError,
|
||||||
|
|
@ -40,6 +43,7 @@ impl IdxError {
|
||||||
Self::SymbolNotFound(_) => ErrorCode::SymbolNotFound,
|
Self::SymbolNotFound(_) => ErrorCode::SymbolNotFound,
|
||||||
Self::RateLimited => ErrorCode::RateLimited,
|
Self::RateLimited => ErrorCode::RateLimited,
|
||||||
Self::ProviderUnavailable => ErrorCode::ProviderUnavailable,
|
Self::ProviderUnavailable => ErrorCode::ProviderUnavailable,
|
||||||
|
Self::Unsupported(_) => ErrorCode::Unsupported,
|
||||||
Self::ParseError(_) => ErrorCode::ParseError,
|
Self::ParseError(_) => ErrorCode::ParseError,
|
||||||
Self::CacheMiss(_) => ErrorCode::CacheMiss,
|
Self::CacheMiss(_) => ErrorCode::CacheMiss,
|
||||||
Self::ConfigError(_) => ErrorCode::ConfigError,
|
Self::ConfigError(_) => ErrorCode::ConfigError,
|
||||||
|
|
@ -65,5 +69,8 @@ mod tests {
|
||||||
|
|
||||||
let parse = IdxError::ParseError("bad json".to_string());
|
let parse = IdxError::ParseError("bad json".to_string());
|
||||||
assert_eq!(parse.code(), ErrorCode::ParseError);
|
assert_eq!(parse.code(), ErrorCode::ParseError);
|
||||||
|
|
||||||
|
let unsupported = IdxError::Unsupported("history unavailable".to_string());
|
||||||
|
assert_eq!(unsupported.code(), ErrorCode::Unsupported);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
tests/cli.rs
25
tests/cli.rs
|
|
@ -106,6 +106,31 @@ fn technical_with_mock_provider_json_contains_fields() {
|
||||||
.stdout(predicate::str::contains("\"signals\""));
|
.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]
|
#[test]
|
||||||
fn growth_with_mock_provider_table_contains_expected_rows() {
|
fn growth_with_mock_provider_table_contains_expected_rows() {
|
||||||
test_bin("growth-table")
|
test_bin("growth-table")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue