indexkit -- index constituent service for Rust, sourced from SEC EDGAR
N-PORT filings.
Monthly snapshots of the S&P 500, S&P MidCap 400, S&P SmallCap 600, Nasdaq-100, and Dow Jones Industrial Average, served from parquet files with runtime GitHub fetch and local cache. No API keys. Public domain data. Offline after the first successful fetch.
Quick start -- one-off scripts
use indexkit::{ym, IndexId};
#[tokio::main]
async fn main() -> indexkit::Result<()> {
// Free functions -- no client setup needed
let sp500 = indexkit::sp500_latest().await?;
let ndx = indexkit::constituents_for(IndexId::Ndx, ym!(2024, 1)).await?;
println!("S&P 500 latest: {} holdings", sp500.len());
println!("Top: {} at {:.2}%", sp500[0].name, sp500[0].weight * 100.0);
println!("NDX Jan 2024: {} holdings", ndx.len());
Ok(())
}
Client pattern -- connection pool + cache reuse
use indexkit::{Indexkit, ym, YearMonth};
#[tokio::main]
async fn main() -> indexkit::Result<()> {
let client = Indexkit::new(); // infallible, no ?
// Any month form works -- no chrono import needed
let a = client.sp500("2024-01").await?;
let b = client.sp500(202401u32).await?;
let c = client.sp500((2024i32, 1u32)).await?;
let d = client.sp500(ym!(2024, 1)).await?;
let e = client.sp500(YearMonth::new(2024, 1)?).await?;
// All equivalent
assert_eq!(a.len(), b.len());
assert_eq!(c.len(), d.len());
let _ = e;
Ok(())
}
Major types
- [
Indexkit] -- stateful client; create once, call many times. - [
YearMonth] -- year-month newtype; accepts strings, integers, tuples. - [
Constituent] -- one holding. - [
IndexSnapshot] -- constituents + metadata for one month. - [
IndexId] -- typed index identifier (Sp500, Sp400, Sp600, Ndx, Dji). - [
Error] -- unified error type; match on this, never on sub-types.
Environment overrides
| Variable | Effect |
|---|---|
INDEXKIT_BASE_URL |
Replace the GitHub raw origin URL |
INDEXKIT_CACHE_DIR |
Override ~/.cache/indexkit/ |
INDEXKIT_MIRROR_URL |
CDN mirror fallback URL (default: jsDelivr) |
Limitations (v1.0)
- No ticker: N-PORT does not include ticker symbols. Every
[
Constituent::ticker] isNone. Use CUSIP (always present) as the join key and enrich downstream via OpenFIGI or a CUSIP->ticker map. - No GICS sector: reserved for v1.1 via SIC -> GICS cross-walk.
- 60-90 day filing lag: ETFs file N-PORT ~60 days after each month's period end, so "latest" is typically two months behind today. This is a regulatory constraint, not a crawler limitation.
- Coverage starts 2019-11: SEC Rule 30b1-9 N-PORT public filing effective date.
Modules
- [
client] -- [Indexkit] async client with blocking wrappers. - [
date] -- [YearMonth] newtype for month inputs. - [
types] -- [Constituent], [IndexSnapshot], [IndexId]. - [
nport] -- N-PORTprimary_doc.xmlparser. - [
cik] -- ETF -> CIK / series mapping (verified against live SEC). - [
parquet_io] -- parquet writer + reader. - [
sec] -- SEC EDGAR client used by the CLI for backfill. - [
error] -- unified [Error] enum and [Result] alias.