mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-08-07 01:33:52 +00:00
fix: handle non-finite msn key ratios
This commit is contained in:
parent
e2c0425a3b
commit
19748d397e
9 changed files with 484 additions and 15 deletions
|
|
@ -31,6 +31,10 @@ pub(crate) fn parse_fundamentals_from_str(
|
|||
mod tests {
|
||||
use super::{parse_fundamentals_from_str, parse_quote_from_str};
|
||||
|
||||
fn minimal_quote_raw() -> &'static str {
|
||||
r#"[{"symbol":"BBCA","marketCap":1215200000000000}]"#
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_quote_fixture_json() {
|
||||
let raw = std::fs::read_to_string("tests/fixtures/msn_quote_bbca.json")
|
||||
|
|
@ -59,4 +63,84 @@ mod tests {
|
|||
assert_eq!(fundamentals.earnings_growth, Some(0.121));
|
||||
assert_eq!(fundamentals.market_cap, Some(1_215_200_000_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_fundamentals_with_infinity_string_as_missing_data() {
|
||||
let raw = r#"[
|
||||
{
|
||||
"companyMetrics": [
|
||||
{
|
||||
"year": "2025",
|
||||
"fiscalPeriodType": "TTM",
|
||||
"priceToEarningsRatio": "Infinity",
|
||||
"priceToBookRatio": 4.6,
|
||||
"roe": 19.8,
|
||||
"profitMargin": 44.2,
|
||||
"debtToEquityRatio": 0.75,
|
||||
"currentRatio": 1.4
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
let fundamentals = parse_fundamentals_from_str(raw, Some(minimal_quote_raw()))
|
||||
.expect("fundamentals parsed");
|
||||
|
||||
assert_eq!(fundamentals.trailing_pe, None);
|
||||
assert_eq!(fundamentals.price_to_book, Some(4.6));
|
||||
assert_eq!(fundamentals.return_on_equity, Some(0.198));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_fundamentals_with_negative_infinity_string_as_missing_data() {
|
||||
let raw = r#"[
|
||||
{
|
||||
"companyMetrics": [
|
||||
{
|
||||
"year": "2025",
|
||||
"fiscalPeriodType": "TTM",
|
||||
"priceToEarningsRatio": 12.5,
|
||||
"debtToEquityRatio": "-Infinity",
|
||||
"roe": 19.8,
|
||||
"profitMargin": 44.2,
|
||||
"currentRatio": 1.4
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
let fundamentals = parse_fundamentals_from_str(raw, Some(minimal_quote_raw()))
|
||||
.expect("fundamentals parsed");
|
||||
|
||||
assert_eq!(fundamentals.trailing_pe, Some(12.5));
|
||||
assert_eq!(fundamentals.debt_to_equity, None);
|
||||
assert_eq!(fundamentals.return_on_equity, Some(0.198));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_fundamentals_with_nan_string_as_missing_data() {
|
||||
let raw = r#"[
|
||||
{
|
||||
"companyMetrics": [
|
||||
{
|
||||
"year": "2025",
|
||||
"fiscalPeriodType": "TTM",
|
||||
"revenueGrowthRate": "NaN",
|
||||
"earningsGrowthRate": 12.1,
|
||||
"roe": 19.8,
|
||||
"profitMargin": 44.2,
|
||||
"debtToEquityRatio": 0.75,
|
||||
"currentRatio": 1.4
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
let fundamentals = parse_fundamentals_from_str(raw, Some(minimal_quote_raw()))
|
||||
.expect("fundamentals parsed");
|
||||
|
||||
assert_eq!(fundamentals.revenue_growth, None);
|
||||
assert_eq!(fundamentals.earnings_growth, Some(0.121));
|
||||
assert_eq!(fundamentals.return_on_equity, Some(0.198));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,33 +47,45 @@ pub(crate) struct KeyRatios {
|
|||
pub(crate) struct IndustryMetric {
|
||||
pub(crate) year: Option<String>,
|
||||
pub(crate) fiscal_period_type: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) revenue_growth_rate: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) earnings_growth_rate: Option<f64>,
|
||||
#[serde(default, rename = "netIncomeYTDYTDGrowthRate")]
|
||||
#[serde(
|
||||
default,
|
||||
rename = "netIncomeYTDYTDGrowthRate",
|
||||
deserialize_with = "de_opt_f64_lenient"
|
||||
)]
|
||||
pub(crate) net_income_ytd_ytd_growth_rate: Option<f64>,
|
||||
#[serde(default, rename = "revenueYTDYTD")]
|
||||
#[serde(
|
||||
default,
|
||||
rename = "revenueYTDYTD",
|
||||
deserialize_with = "de_opt_f64_lenient"
|
||||
)]
|
||||
pub(crate) revenue_ytd_ytd: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) net_margin: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) profit_margin: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) roe: Option<f64>,
|
||||
#[serde(default, rename = "roaTTM")]
|
||||
#[serde(default, rename = "roaTTM", deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) roa_ttm: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) return_on_asset_current: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) debt_to_equity_ratio: Option<f64>,
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) current_ratio: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) price_to_earnings_ratio: Option<f64>,
|
||||
#[serde(default, rename = "forwardPriceToEPS")]
|
||||
#[serde(
|
||||
default,
|
||||
rename = "forwardPriceToEPS",
|
||||
deserialize_with = "de_opt_f64_lenient"
|
||||
)]
|
||||
pub(crate) forward_price_to_eps: Option<f64>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "de_opt_f64_lenient")]
|
||||
pub(crate) price_to_book_ratio: Option<f64>,
|
||||
}
|
||||
|
||||
|
|
@ -324,10 +336,23 @@ where
|
|||
Some(NumberLike::F64(_)) => Ok(None),
|
||||
Some(NumberLike::String(raw)) => {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("nan") {
|
||||
if trimmed.is_empty()
|
||||
|| trimmed.eq_ignore_ascii_case("nan")
|
||||
|| trimmed.eq_ignore_ascii_case("infinity")
|
||||
|| trimmed.eq_ignore_ascii_case("-infinity")
|
||||
{
|
||||
Ok(None)
|
||||
} else {
|
||||
trimmed.parse::<f64>().map(Some).map_err(D::Error::custom)
|
||||
trimmed
|
||||
.parse::<f64>()
|
||||
.map_err(D::Error::custom)
|
||||
.map(|number| {
|
||||
if number.is_finite() {
|
||||
Some(number)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
None => Ok(None),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue