mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-08-07 01:33:52 +00:00
fix(screen): debug the msn api, remove probelamtic msn api, filtering via client side
This commit is contained in:
parent
69ca349c49
commit
7d32e9d69d
3 changed files with 171 additions and 15 deletions
|
|
@ -394,12 +394,12 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
|
|||
.as_ref()
|
||||
.ok_or_else(|| IdxError::ParseError("no screener data".into()))?;
|
||||
|
||||
// Build Quote directly from screener MsnQuote data; skip stocks with no price
|
||||
// Build Quote directly from screener MsnQuote data; default price to 0 if missing
|
||||
// (do not route through parse_quote which errors on missing price)
|
||||
let results: Vec<Quote> = quotes
|
||||
.iter()
|
||||
.filter_map(|q| {
|
||||
let raw_price = q.price?; // skip if no price
|
||||
.map(|q| {
|
||||
let raw_price = q.price.unwrap_or(0.0);
|
||||
let price = round_price(raw_price);
|
||||
let prev_close = q.price_previous_close.map(round_price);
|
||||
let change = prev_close
|
||||
|
|
@ -425,7 +425,7 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
|
|||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
Some(Quote {
|
||||
Quote {
|
||||
symbol: normalized_symbol(&ticker, &ticker),
|
||||
price,
|
||||
change,
|
||||
|
|
@ -438,7 +438,7 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
|
|||
range_signal,
|
||||
prev_close,
|
||||
avg_volume: round_u64(q.average_volume),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,17 @@ pub enum Shell {
|
|||
long_about = "idx-cli is a Rust command-line tool for Indonesian stock market (IDX) analysis.\nIt supports quote lookup, historical data, local caching, and output formats for both humans and AI agents."
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[arg(short, long, value_enum, global = true)]
|
||||
#[arg(short, long, value_enum, global = true, help = "Output format")]
|
||||
pub output: Option<OutputFormat>,
|
||||
#[arg(long, global = true)]
|
||||
#[arg(long, global = true, help = "Disable colored output")]
|
||||
pub no_color: bool,
|
||||
#[arg(short, long, global = true)]
|
||||
#[arg(short, long, global = true, help = "Suppress non-essential output")]
|
||||
pub quiet: bool,
|
||||
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
|
||||
#[arg(short, long, global = true, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv)")]
|
||||
pub verbose: u8,
|
||||
#[arg(long, global = true)]
|
||||
#[arg(long, global = true, help = "Run without network requests")]
|
||||
pub offline: bool,
|
||||
#[arg(long, global = true)]
|
||||
#[arg(long, global = true, help = "Bypass local cache")]
|
||||
pub no_cache: bool,
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use clap::{Args, Subcommand};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
|
|
@ -427,9 +429,15 @@ pub fn handle(
|
|||
let msn = MsnProvider::new(false);
|
||||
let filter_key = screener_filter_key(filter);
|
||||
let region_key = screener_region_key(region);
|
||||
let quotes: Vec<Quote> = fetch_msn_only("screen", config.provider, || {
|
||||
msn.screener(filter_key, region_key, *limit)
|
||||
// For filters that fall back to topperfs, fetch all stocks so
|
||||
// client-side sorting picks the correct top N.
|
||||
let needs_full_fetch = matches!(filter.as_str(), "high-volume" | "large-cap");
|
||||
let fetch_limit = if needs_full_fetch { 500 } else { *limit };
|
||||
let mut quotes: Vec<Quote> = fetch_msn_only("screen", config.provider, || {
|
||||
msn.screener(filter_key, region_key, fetch_limit)
|
||||
})?;
|
||||
sort_screener_quotes(&mut quotes, filter);
|
||||
quotes.truncate(*limit);
|
||||
render_screener("es, &config.output, config.no_color)
|
||||
}
|
||||
StocksSubcommand::Compare { symbols } => {
|
||||
|
|
@ -644,8 +652,8 @@ fn screener_filter_key(filter: &str) -> &'static str {
|
|||
"low-pe" => "st_list_lowpe",
|
||||
"52w-high" => "st_list_52wkhi",
|
||||
"52w-low" => "st_list_52wklow",
|
||||
"high-volume" => "st_list_highvol",
|
||||
"large-cap" => "st_list_largecap",
|
||||
"high-volume" => "st_list_topperfs",
|
||||
"large-cap" => "st_list_topperfs",
|
||||
_ => "st_list_topperfs",
|
||||
}
|
||||
}
|
||||
|
|
@ -661,6 +669,46 @@ fn screener_region_key(region: &str) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
fn sort_screener_quotes(quotes: &mut [Quote], filter: &str) {
|
||||
match filter {
|
||||
"top-performers" => {
|
||||
quotes.sort_by(|a, b| {
|
||||
b.change_pct
|
||||
.partial_cmp(&a.change_pct)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
});
|
||||
}
|
||||
"worst-performers" => {
|
||||
quotes.sort_by(|a, b| {
|
||||
a.change_pct
|
||||
.partial_cmp(&b.change_pct)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
});
|
||||
}
|
||||
"52w-high" => {
|
||||
quotes.sort_by(|a, b| {
|
||||
let pa = a.week52_position.unwrap_or(f64::MIN);
|
||||
let pb = b.week52_position.unwrap_or(f64::MIN);
|
||||
pb.partial_cmp(&pa).unwrap_or(Ordering::Equal)
|
||||
});
|
||||
}
|
||||
"52w-low" => {
|
||||
quotes.sort_by(|a, b| {
|
||||
let pa = a.week52_position.unwrap_or(f64::MAX);
|
||||
let pb = b.week52_position.unwrap_or(f64::MAX);
|
||||
pa.partial_cmp(&pb).unwrap_or(Ordering::Equal)
|
||||
});
|
||||
}
|
||||
"high-volume" => {
|
||||
quotes.sort_by(|a, b| b.volume.cmp(&a.volume));
|
||||
}
|
||||
"large-cap" => {
|
||||
quotes.sort_by(|a, b| b.market_cap.unwrap_or(0).cmp(&a.market_cap.unwrap_or(0)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Days, NaiveDate};
|
||||
|
|
@ -696,4 +744,112 @@ mod tests {
|
|||
assert!(report.volume.ratio20.is_some());
|
||||
assert_eq!(report.signals.trend, Signal::Neutral);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_screener_top_performers_descending() {
|
||||
use crate::api::types::Quote;
|
||||
|
||||
let mut quotes = vec![
|
||||
Quote {
|
||||
symbol: "A".into(),
|
||||
price: 100,
|
||||
change: 1,
|
||||
change_pct: 1.0,
|
||||
volume: 100,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close: None,
|
||||
avg_volume: None,
|
||||
},
|
||||
Quote {
|
||||
symbol: "B".into(),
|
||||
price: 200,
|
||||
change: 10,
|
||||
change_pct: 5.0,
|
||||
volume: 200,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close: None,
|
||||
avg_volume: None,
|
||||
},
|
||||
Quote {
|
||||
symbol: "C".into(),
|
||||
price: 150,
|
||||
change: 5,
|
||||
change_pct: 3.0,
|
||||
volume: 150,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close: None,
|
||||
avg_volume: None,
|
||||
},
|
||||
];
|
||||
|
||||
super::sort_screener_quotes(&mut quotes, "top-performers");
|
||||
let pcts: Vec<f64> = quotes.iter().map(|q| q.change_pct).collect();
|
||||
assert_eq!(pcts, vec![5.0, 3.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_screener_worst_performers_ascending() {
|
||||
use crate::api::types::Quote;
|
||||
|
||||
let mut quotes = vec![
|
||||
Quote {
|
||||
symbol: "A".into(),
|
||||
price: 100,
|
||||
change: 1,
|
||||
change_pct: 1.0,
|
||||
volume: 100,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close: None,
|
||||
avg_volume: None,
|
||||
},
|
||||
Quote {
|
||||
symbol: "B".into(),
|
||||
price: 200,
|
||||
change: -10,
|
||||
change_pct: -5.0,
|
||||
volume: 200,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close: None,
|
||||
avg_volume: None,
|
||||
},
|
||||
Quote {
|
||||
symbol: "C".into(),
|
||||
price: 150,
|
||||
change: -3,
|
||||
change_pct: -2.0,
|
||||
volume: 150,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close: None,
|
||||
avg_volume: None,
|
||||
},
|
||||
];
|
||||
|
||||
super::sort_screener_quotes(&mut quotes, "worst-performers");
|
||||
let pcts: Vec<f64> = quotes.iter().map(|q| q.change_pct).collect();
|
||||
assert_eq!(pcts, vec![-5.0, -2.0, 1.0]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue