1use super::config::SyncConfig;
4use super::http::Fetcher;
5use super::industry::flag;
6use super::util::num;
7use super::HttpClient;
8use super::FMP_BASE;
9
10pub const US_EXCHANGES: &str = "NASDAQ,NYSE,AMEX";
13
14#[derive(Default)]
16pub struct SymbolFilter {
17 pub min_market_cap: f64,
19 pub exchange: Option<String>,
22 pub include_etf: bool,
24 pub limit: Option<usize>,
26}
27
28pub fn build_symbol_list<H: HttpClient>(
34 http: &H,
35 api_key: &str,
36 cfg: &SyncConfig,
37 filter: &SymbolFilter,
38) -> Result<Vec<String>, String> {
39 let fetcher = Fetcher::new(http, cfg);
40 let mut url = format!("{FMP_BASE}/stable/company-screener?apikey={api_key}");
41 if filter.min_market_cap > 0.0 {
42 url.push_str(&format!(
43 "&marketCapMoreThan={}",
44 filter.min_market_cap as u64
45 ));
46 }
47 if !filter.include_etf {
48 url.push_str("&isEtf=false&isFund=false");
49 }
50 if let Some(ex) = &filter.exchange {
51 let ex = ex.trim();
52 if !ex.is_empty() && !ex.eq_ignore_ascii_case("all") {
54 url.push_str(&format!("&exchange={ex}"));
55 }
56 }
57 if let Some(n) = filter.limit {
58 url.push_str(&format!("&limit={n}"));
59 }
60 let rows = fetcher.get_rows(&url)?;
61 let mut syms: Vec<String> = rows
62 .iter()
63 .filter_map(|r| {
64 let obj = r.as_object()?;
65 if !filter.include_etf && (flag(obj, "isEtf") || flag(obj, "isFund")) {
67 return None;
68 }
69 if filter.min_market_cap > 0.0 {
70 if let Some(mc) = num(obj, &["marketCap", "marketCapitalization"]) {
71 if mc < filter.min_market_cap {
72 return None;
73 }
74 }
75 }
76 obj.get("symbol")?
77 .as_str()
78 .map(str::trim)
79 .filter(|s| !s.is_empty())
80 .map(str::to_string)
81 })
82 .collect();
83 syms.sort();
84 syms.dedup();
85 Ok(syms)
86}
87
88pub fn parse_market_cap(s: &str) -> Result<f64, String> {
93 let s = s.trim();
94 if s.is_empty() {
95 return Err("empty market-cap value".to_string());
96 }
97 let mult = match s.chars().last().unwrap().to_ascii_lowercase() {
98 'k' => 1e3,
99 'm' => 1e6,
100 'b' => 1e9,
101 't' => 1e12,
102 _ => 1.0,
103 };
104 let digits = if mult == 1.0 { s } else { &s[..s.len() - 1] };
106 let val: f64 = digits
107 .trim()
108 .parse()
109 .map_err(|_| format!("invalid market cap '{s}' (try 1b, 500m, or a plain number)"))?;
110 if val < 0.0 || !val.is_finite() {
111 return Err(format!("market cap must be a non-negative number: '{s}'"));
112 }
113 Ok(val * mult)
114}
115
116pub fn parse_symbols_list(text: &str) -> Vec<String> {
120 text.lines()
121 .filter_map(|line| {
122 let line = line.trim();
123 if line.is_empty() || line.starts_with('#') {
124 return None;
125 }
126 let first = line.split(',').next()?.trim();
127 if first.is_empty() || first.eq_ignore_ascii_case("symbol") {
128 return None;
129 }
130 Some(first.to_string())
131 })
132 .collect()
133}