use chrono::{Datelike, NaiveDate};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::error::{Error, Result};
use crate::sources::parquet_io::{read_sofr_year, read_treasury_year};
use crate::tenor::Tenor;
fn data_dir() -> PathBuf {
if let Ok(env_dir) = std::env::var("CURVEKIT_DATA_DIR") {
return PathBuf::from(env_dir);
}
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("data")
}
pub fn treasury_curve(date: NaiveDate) -> Result<crate::YieldCurve> {
let year = date.year();
let path = data_dir().join(format!("treasury-{year}.parquet"));
if !path.exists() {
return Err(Error::DateNotFound(format!(
"treasury-{year}.parquet not found (run: curvekit backfill)"
)));
}
let curves = read_treasury_year(&path)?;
curves
.into_iter()
.find(|c| c.date == date)
.ok_or_else(|| Error::DateNotFound(format!("no treasury curve for {date}")))
}
pub fn sofr(date: NaiveDate) -> Result<f64> {
let year = date.year();
let path = data_dir().join(format!("sofr-{year}.parquet"));
if !path.exists() {
return Err(Error::DateNotFound(format!(
"sofr-{year}.parquet not found (run: curvekit backfill)"
)));
}
let rates = read_sofr_year(&path)?;
rates
.into_iter()
.find(|r| r.date == date)
.map(|r| r.rate)
.ok_or_else(|| Error::DateNotFound(format!("no SOFR for {date}")))
}
pub fn rate_for(date: NaiveDate, tenor: impl Into<Tenor>) -> Result<f64> {
let tenor = tenor.into();
let curve = treasury_curve(date)?;
curve
.get(tenor)
.ok_or_else(|| Error::Interpolation(format!("empty curve for {date}")))
}
#[deprecated(
since = "0.2.0",
note = "use `rate_for(date, Tenor::days(d))` or `rate_for(date, Tenor::Y10)` instead"
)]
pub fn rate_for_days(date: NaiveDate, days: u32) -> Result<f64> {
rate_for(date, days)
}
pub fn treasury_latest_date() -> NaiveDate {
latest_date_for("treasury").unwrap_or_else(|| NaiveDate::from_ymd_opt(2000, 1, 3).unwrap())
}
pub fn sofr_latest_date() -> NaiveDate {
latest_date_for("sofr").unwrap_or_else(|| NaiveDate::from_ymd_opt(2018, 4, 2).unwrap())
}
fn latest_date_for(prefix: &str) -> Option<NaiveDate> {
let dir = data_dir();
if !dir.exists() {
return None;
}
let mut max_date: Option<NaiveDate> = None;
let read_dir = std::fs::read_dir(&dir).ok()?;
for entry in read_dir.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !name_str.starts_with(prefix) || !name_str.ends_with(".parquet") {
continue;
}
let year_str = name_str
.strip_prefix(&format!("{prefix}-"))
.and_then(|s| s.strip_suffix(".parquet"))?;
let year: i32 = year_str.parse().ok()?;
let path = dir.join(name.clone());
let date = if prefix == "treasury" {
read_treasury_year(&path)
.ok()
.and_then(|v| v.into_iter().map(|c| c.date).max())
} else {
read_sofr_year(&path)
.ok()
.and_then(|v| v.into_iter().map(|r| r.date).max())
};
let candidate = date.unwrap_or_else(|| {
NaiveDate::from_ymd_opt(year, 12, 31)
.unwrap_or(NaiveDate::from_ymd_opt(year, 1, 1).unwrap())
});
max_date = Some(match max_date {
Some(cur) if cur >= candidate => cur,
_ => candidate,
});
}
max_date
}
pub fn treasury_continuous_map(date: NaiveDate) -> Result<HashMap<u32, f64>> {
Ok(treasury_curve(date)?.to_continuous_map())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn treasury_latest_date_does_not_panic() {
let _ = treasury_latest_date();
}
#[test]
fn sofr_latest_date_does_not_panic() {
let _ = sofr_latest_date();
}
#[test]
fn treasury_curve_missing_year_returns_err() {
let date = NaiveDate::from_ymd_opt(1800, 1, 1).unwrap();
let result = treasury_curve(date);
assert!(result.is_err());
}
#[test]
fn sofr_missing_year_returns_err() {
let date = NaiveDate::from_ymd_opt(1800, 1, 1).unwrap();
let result = sofr(date);
assert!(result.is_err());
}
}