use std::collections::HashSet;
use serde_json::Value;
use super::config::SyncConfig;
use super::http::Fetcher;
use super::util::iso_to_i32;
use super::HttpClient;
use super::FMP_BASE;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DelistedSymbol {
pub symbol: String,
pub exchange: String,
pub delisted_date: Option<i32>,
}
pub(crate) const MAX_DELISTED_PAGES: u32 = 200;
pub(crate) fn delisted_url(page: u32, key: &str) -> String {
format!("{FMP_BASE}/stable/delisted-companies?page={page}&apikey={key}")
}
pub(crate) fn parse_delisted_rows(rows: &[Value]) -> Vec<DelistedSymbol> {
rows.iter()
.filter_map(|r| {
let obj = r.as_object()?;
let symbol = obj
.get("symbol")?
.as_str()
.map(str::trim)
.filter(|s| !s.is_empty())?
.to_string();
let exchange = obj
.get("exchange")
.and_then(Value::as_str)
.unwrap_or("")
.trim()
.to_string();
let delisted_date = obj
.get("delistedDate")
.and_then(Value::as_str)
.and_then(iso_to_i32);
Some(DelistedSymbol {
symbol,
exchange,
delisted_date,
})
})
.collect()
}
pub(crate) fn exchange_filter(spec: Option<&str>) -> Option<HashSet<String>> {
let spec = spec?.trim();
if spec.is_empty() || spec.eq_ignore_ascii_case("all") {
return None;
}
let set: HashSet<String> = spec
.split(',')
.map(|s| s.trim().to_ascii_uppercase())
.filter(|s| !s.is_empty())
.collect();
if set.is_empty() {
None
} else {
Some(set)
}
}
pub(crate) fn keep_exchange(wanted: &Option<HashSet<String>>, exchange: &str) -> bool {
match wanted {
None => true,
Some(set) => set.contains(&exchange.trim().to_ascii_uppercase()),
}
}
pub fn fetch_delisted<H: HttpClient>(
http: &H,
api_key: &str,
cfg: &SyncConfig,
exchanges: Option<&str>,
) -> Result<Vec<DelistedSymbol>, String> {
let fetcher = Fetcher::new(http, cfg);
let wanted = exchange_filter(exchanges);
let mut out: Vec<DelistedSymbol> = Vec::new();
for page in 0..MAX_DELISTED_PAGES {
let rows = fetcher.get_rows(&delisted_url(page, api_key))?;
if rows.is_empty() {
break;
}
for d in parse_delisted_rows(&rows) {
if keep_exchange(&wanted, &d.exchange) {
out.push(d);
}
}
if page == MAX_DELISTED_PAGES - 1 {
eprintln!(
"delisted: hit page cap ({MAX_DELISTED_PAGES}); list may be truncated (older names dropped)"
);
}
}
out.sort_by(|a, b| a.symbol.cmp(&b.symbol));
out.dedup_by(|a, b| a.symbol == b.symbol);
Ok(out)
}