mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35:36 +00:00
fix(quote): report listed-but-unpriced stocks as NOMARKETDATA
MSN returns suspended names (21 of 896 on 2026-10-02, e.g. WIKA, SUPR) with symbol, name and market cap but no price. These were reported as SYMBOLNOTFOUND, which tells an agent the ticker does not exist. Add a NoMarketData error for this case; unknown tickers stay SYMBOLNOTFOUND. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ld3fDSDiBzAaQvgZJX5NKM
This commit is contained in:
parent
f77f81c97b
commit
1785ac5c52
2 changed files with 31 additions and 6 deletions
|
|
@ -15,9 +15,16 @@ use std::collections::HashMap;
|
|||
|
||||
pub(super) fn parse_quote(symbol: &str, quotes: &[MsnQuote]) -> Result<Quote, IdxError> {
|
||||
let quote = quotes.first().ok_or(IdxError::ProviderUnavailable)?;
|
||||
let raw_price = quote
|
||||
.price
|
||||
.ok_or_else(|| IdxError::SymbolNotFound(symbol.to_string()))?;
|
||||
let Some(raw_price) = quote.price else {
|
||||
// MSN still lists suspended/untraded names (symbol, name, market cap)
|
||||
// but sends no price. That is a real instrument with no market data.
|
||||
if quote.symbol.is_some() {
|
||||
return Err(IdxError::NoMarketData(format!(
|
||||
"{symbol} is listed but has no price from MSN (it may be suspended)"
|
||||
)));
|
||||
}
|
||||
return Err(IdxError::SymbolNotFound(symbol.to_string()));
|
||||
};
|
||||
let prev_close = quote.price_previous_close.map(round_price);
|
||||
let price = round_price(raw_price);
|
||||
let change = prev_close
|
||||
|
|
@ -778,9 +785,9 @@ fn collect_earnings(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
KeyRatios, RawChartResponse, RawFinancialStatement, RawNewsFeed, RawScreenerResponse,
|
||||
RawSentiment, parse_financial_statements, parse_fundamentals, parse_history, parse_news,
|
||||
parse_screener_results, parse_sentiment,
|
||||
KeyRatios, MsnQuote, RawChartResponse, RawFinancialStatement, RawNewsFeed,
|
||||
RawScreenerResponse, RawSentiment, parse_financial_statements, parse_fundamentals,
|
||||
parse_history, parse_news, parse_quote, parse_screener_results, parse_sentiment,
|
||||
};
|
||||
use crate::error::IdxError;
|
||||
|
||||
|
|
@ -935,6 +942,20 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listed_quote_without_price_is_no_market_data() {
|
||||
let raw: Vec<MsnQuote> = serde_json::from_str(
|
||||
r#"[{"symbol":"WIKA","shortName":"Wijaya Karya","marketCap":8080746000000.0}]"#,
|
||||
)
|
||||
.expect("quote should deserialize");
|
||||
let err = parse_quote("WIKA.JK", &raw).unwrap_err();
|
||||
assert!(matches!(err, IdxError::NoMarketData(_)), "{err:?}");
|
||||
|
||||
let raw: Vec<MsnQuote> = serde_json::from_str("[{}]").expect("quote should deserialize");
|
||||
let err = parse_quote("ZZZZ.JK", &raw).unwrap_err();
|
||||
assert!(matches!(err, IdxError::SymbolNotFound(_)), "{err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_sentiment_response_is_empty_data_not_an_error() {
|
||||
let sentiment = parse_sentiment("BBCA", &[]).expect("empty sentiment is valid");
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ pub enum IdxError {
|
|||
PdfParseError(String),
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("no market data: {0}")]
|
||||
NoMarketData(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||
|
|
@ -50,6 +52,7 @@ pub enum ErrorCode {
|
|||
DatabaseError,
|
||||
PdfParseError,
|
||||
InvalidInput,
|
||||
NoMarketData,
|
||||
}
|
||||
|
||||
impl IdxError {
|
||||
|
|
@ -69,6 +72,7 @@ impl IdxError {
|
|||
Self::DatabaseError(_) => ErrorCode::DatabaseError,
|
||||
Self::PdfParseError(_) => ErrorCode::PdfParseError,
|
||||
Self::InvalidInput(_) => ErrorCode::InvalidInput,
|
||||
Self::NoMarketData(_) => ErrorCode::NoMarketData,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue