repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

2.2 KB · 61 lines · Rust Raw History
 1//! S&P 500 membership, used to restrict the dashboard's market-movers lists to
 2//! recognizable large-cap names rather than the whole-market micro-caps Yahoo's
 3//! predefined screeners return (a user request: "market movers has a lot of
 4//! companies I've literally never heard of").
 5//!
 6//! The constituent list lives in `universe/sp500.txt` (one ticker per line, in
 7//! Yahoo symbology so `BRK.B` is `BRK-B`; `#` comments and blank lines allowed)
 8//! and is embedded at compile time, so there is no runtime file IO and it ships
 9//! inside the binary. Refresh by editing that file and redeploying (the file's
10//! header documents the one-liner that regenerates it).
11
12use std::collections::HashSet;
13use std::sync::LazyLock;
14
15/// The raw constituent list, embedded at build time.
16const SP500_RAW: &str = include_str!("../universe/sp500.txt");
17
18/// The membership set, parsed once on first use. Tickers are uppercased so the
19/// lookup is case-insensitive; blank lines and `#` comments are skipped.
20static SP500: LazyLock<HashSet<String>> = LazyLock::new(|| {
21    SP500_RAW
22        .lines()
23        .map(str::trim)
24        .filter(|l| !l.is_empty() && !l.starts_with('#'))
25        .map(str::to_uppercase)
26        .collect()
27});
28
29/// Whether `ticker` is an S&P 500 constituent (case-insensitive).
30pub fn is_member(ticker: &str) -> bool {
31    SP500.contains(&ticker.to_uppercase())
32}
33
34/// How many constituents are loaded — for a boot log / sanity check.
35pub fn count() -> usize {
36    SP500.len()
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn loads_a_full_roster() {
45        // The S&P 500 hovers around 500-505 names; guard against an empty or
46        // truncated embed (e.g. a botched refresh).
47        assert!(count() >= 490, "expected ~500 constituents, got {}", count());
48    }
49
50    #[test]
51    fn known_members_and_non_members() {
52        assert!(is_member("AAPL"));
53        assert!(is_member("aapl"), "lookup is case-insensitive");
54        assert!(is_member("BRK-B"), "dotted tickers stored in Yahoo dash form");
55        assert!(is_member("JPM"));
56        // A real ticker that is not in the index, and obvious junk.
57        assert!(!is_member("SPCX"), "a delisted micro-cap fund is not a member");
58        assert!(!is_member("NOTATICKER"));
59    }
60}