use chrono::Datelike;
use futures::future::try_join_all;
use std::path::PathBuf;
use crate::curve::{SofrDay, YieldCurve};
use crate::date::{Date, IntoDate};
use crate::daycount::DayCount;
use crate::error::{Error, Result};
use crate::fetcher::{default_cache_dir, resolved_base_url, CachedFetcher};
use crate::sources::effr::EffrDay;
use crate::sources::obfr::ObfrDay;
use crate::sources::parquet_io::{
read_effr_year, read_obfr_year, read_sofr_year, read_treasury_year,
};
use crate::tenor::Tenor;
pub struct Curvekit {
fetcher: CachedFetcher,
}
impl Curvekit {
pub fn new() -> Self {
let http = reqwest::Client::builder()
.user_agent("curvekit/1.0 (+https://github.com/userFRM/curvekit)")
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self {
fetcher: CachedFetcher::new(http, resolved_base_url(), default_cache_dir()),
}
}
pub fn try_new() -> Result<Self> {
let http = reqwest::Client::builder()
.user_agent("curvekit/1.0 (+https://github.com/userFRM/curvekit)")
.timeout(std::time::Duration::from_secs(30))
.build()?;
Ok(Self {
fetcher: CachedFetcher::new(http, resolved_base_url(), default_cache_dir()),
})
}
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.fetcher.set_base_url(url.into());
self
}
pub fn with_cache_dir(mut self, dir: PathBuf) -> Self {
self.fetcher.set_cache_dir(dir);
self
}
#[deprecated(
since = "1.0.0",
note = "use `treasury_par_curve` or `treasury_zero_curve` for explicit yield type"
)]
pub async fn treasury_curve(&self, date: impl IntoDate) -> Result<YieldCurve> {
let date = date.into_date()?;
let year = date.inner().year();
let curves = self.treasury_year(year).await?;
curves
.into_iter()
.find(|c| c.date == date.inner())
.ok_or_else(|| Error::DateNotFound(format!("no treasury curve for {date}")))
}
pub async fn treasury_par_curve(&self, date: impl IntoDate) -> Result<YieldCurve> {
let date = date.into_date()?;
let year = date.inner().year();
let curves = self.treasury_year(year).await?;
curves
.into_iter()
.find(|c| c.date == date.inner())
.ok_or_else(|| Error::DateNotFound(format!("no treasury curve for {date}")))
}
pub async fn treasury_zero_curve(&self, date: impl IntoDate) -> Result<YieldCurve> {
let par = self.treasury_par_curve(date).await?;
par.bootstrap_zero()
}
pub async fn treasury_rate(&self, date: impl IntoDate, tenor: impl Into<Tenor>) -> Result<f64> {
let tenor = tenor.into();
let curve = self.treasury_par_curve(date).await?;
curve
.get(tenor)
.ok_or_else(|| Error::Interpolation(format!("no treasury data at {}", tenor)))
}
pub async fn treasury_rate_with_convention(
&self,
date: impl IntoDate,
tenor: impl Into<Tenor>,
convention: DayCount,
) -> Result<f64> {
let date_val = date.into_date()?;
let tenor = tenor.into();
let curve = self.treasury_par_curve(date_val).await?;
let r = curve
.get(tenor)
.ok_or_else(|| Error::Interpolation(format!("no treasury data at {}", tenor)))?;
let maturity = date_val
.inner()
.checked_add_signed(chrono::Duration::days(tenor.as_days() as i64))
.ok_or_else(|| Error::Other("tenor overflow computing maturity date".into()))?;
let t_act365 = DayCount::Act365Fixed.year_fraction(date_val.inner(), maturity);
let t_conv = convention.year_fraction(date_val.inner(), maturity);
if t_conv == 0.0 {
return Ok(r);
}
Ok(r * t_act365 / t_conv)
}
pub async fn treasury_range(
&self,
start: impl IntoDate,
end: impl IntoDate,
) -> Result<Vec<YieldCurve>> {
let start = start.into_date()?;
let end = end.into_date()?;
if start > end {
return Err(Error::Other(format!(
"treasury_range: start {start} > end {end}"
)));
}
let start_nd = start.inner();
let end_nd = end.inner();
let years: Vec<i32> = (start_nd.year()..=end_nd.year()).collect();
let fetches = years
.iter()
.map(|&y| self.treasury_year(y))
.collect::<Vec<_>>();
let all_years = try_join_all(fetches).await?;
let mut out: Vec<YieldCurve> = all_years
.into_iter()
.flatten()
.filter(|c| c.date >= start_nd && c.date <= end_nd)
.collect();
out.sort_by_key(|c| c.date);
Ok(out)
}
pub async fn treasury_latest(&self) -> Result<YieldCurve> {
use chrono::Utc;
let current_year = Utc::now().year();
for year in [current_year, current_year - 1] {
if let Ok(curves) = self.treasury_year(year).await {
if let Some(latest) = curves.into_iter().max_by_key(|c| c.date) {
return Ok(latest);
}
}
}
Err(Error::DateNotFound("no treasury data available".into()))
}
pub async fn treasury_earliest_date(&self) -> Result<chrono::NaiveDate> {
let curves = self.treasury_year(2000).await?;
curves
.into_iter()
.map(|c| c.date)
.min()
.ok_or_else(|| Error::DateNotFound("no data in treasury-2000.parquet".into()))
}
pub async fn sofr(&self, date: impl IntoDate) -> Result<f64> {
let date = date.into_date()?;
let year = date.inner().year();
let rates = self.sofr_year(year).await?;
rates
.into_iter()
.find(|r| r.date == date.inner())
.map(|r| r.rate)
.ok_or_else(|| Error::DateNotFound(format!("no SOFR for {date}")))
}
pub async fn sofr_range(
&self,
start: impl IntoDate,
end: impl IntoDate,
) -> Result<Vec<SofrDay>> {
let start = start.into_date()?;
let end = end.into_date()?;
if start > end {
return Err(Error::Other(format!(
"sofr_range: start {start} > end {end}"
)));
}
let start_nd = start.inner();
let end_nd = end.inner();
let years: Vec<i32> = (start_nd.year()..=end_nd.year()).collect();
let fetches = years.iter().map(|&y| self.sofr_year(y)).collect::<Vec<_>>();
let all_years = try_join_all(fetches).await?;
let mut out: Vec<SofrDay> = all_years
.into_iter()
.flatten()
.filter(|r| r.date >= start_nd && r.date <= end_nd)
.collect();
out.sort_by_key(|r| r.date);
Ok(out)
}
pub async fn sofr_latest(&self) -> Result<SofrDay> {
use chrono::Utc;
let current_year = Utc::now().year();
for year in [current_year, current_year - 1] {
if let Ok(rates) = self.sofr_year(year).await {
if let Some(latest) = rates.into_iter().max_by_key(|r| r.date) {
return Ok(latest);
}
}
}
Err(Error::DateNotFound("no SOFR data available".into()))
}
pub async fn sofr_earliest_date(&self) -> Result<chrono::NaiveDate> {
let rates = self.sofr_year(2018).await?;
rates
.into_iter()
.map(|r| r.date)
.min()
.ok_or_else(|| Error::DateNotFound("no data in sofr-2018.parquet".into()))
}
pub async fn effr(&self, date: impl IntoDate) -> Result<f64> {
let date = date.into_date()?;
let year = date.inner().year();
let rates = self.effr_year(year).await?;
rates
.into_iter()
.find(|r| r.date == date.inner())
.map(|r| r.rate)
.ok_or_else(|| Error::DateNotFound(format!("no EFFR for {date}")))
}
pub async fn effr_range(
&self,
start: impl IntoDate,
end: impl IntoDate,
) -> Result<Vec<EffrDay>> {
let start = start.into_date()?;
let end = end.into_date()?;
if start > end {
return Err(Error::Other(format!(
"effr_range: start {start} > end {end}"
)));
}
let start_nd = start.inner();
let end_nd = end.inner();
let years: Vec<i32> = (start_nd.year()..=end_nd.year()).collect();
let fetches = years.iter().map(|&y| self.effr_year(y)).collect::<Vec<_>>();
let all_years = futures::future::try_join_all(fetches).await?;
let mut out: Vec<EffrDay> = all_years
.into_iter()
.flatten()
.filter(|r| r.date >= start_nd && r.date <= end_nd)
.collect();
out.sort_by_key(|r| r.date);
Ok(out)
}
pub async fn effr_latest(&self) -> Result<EffrDay> {
use chrono::Utc;
let current_year = Utc::now().year();
for year in [current_year, current_year - 1] {
if let Ok(rates) = self.effr_year(year).await {
if let Some(latest) = rates.into_iter().max_by_key(|r| r.date) {
return Ok(latest);
}
}
}
Err(Error::DateNotFound("no EFFR data available".into()))
}
pub async fn effr_earliest_date(&self) -> Result<chrono::NaiveDate> {
let rates = self.effr_year(2000).await?;
rates
.into_iter()
.map(|r| r.date)
.min()
.ok_or_else(|| Error::DateNotFound("no data in effr-2000.parquet".into()))
}
pub async fn obfr(&self, date: impl IntoDate) -> Result<f64> {
let date = date.into_date()?;
let year = date.inner().year();
let rates = self.obfr_year(year).await?;
rates
.into_iter()
.find(|r| r.date == date.inner())
.map(|r| r.rate)
.ok_or_else(|| Error::DateNotFound(format!("no OBFR for {date}")))
}
pub async fn obfr_range(
&self,
start: impl IntoDate,
end: impl IntoDate,
) -> Result<Vec<ObfrDay>> {
let start = start.into_date()?;
let end = end.into_date()?;
if start > end {
return Err(Error::Other(format!(
"obfr_range: start {start} > end {end}"
)));
}
let start_nd = start.inner();
let end_nd = end.inner();
let years: Vec<i32> = (start_nd.year()..=end_nd.year()).collect();
let fetches = years.iter().map(|&y| self.obfr_year(y)).collect::<Vec<_>>();
let all_years = futures::future::try_join_all(fetches).await?;
let mut out: Vec<ObfrDay> = all_years
.into_iter()
.flatten()
.filter(|r| r.date >= start_nd && r.date <= end_nd)
.collect();
out.sort_by_key(|r| r.date);
Ok(out)
}
pub async fn obfr_latest(&self) -> Result<ObfrDay> {
use chrono::Utc;
let current_year = Utc::now().year();
for year in [current_year, current_year - 1] {
if let Ok(rates) = self.obfr_year(year).await {
if let Some(latest) = rates.into_iter().max_by_key(|r| r.date) {
return Ok(latest);
}
}
}
Err(Error::DateNotFound("no OBFR data available".into()))
}
pub async fn obfr_earliest_date(&self) -> Result<chrono::NaiveDate> {
let rates = self.obfr_year(2016).await?;
rates
.into_iter()
.map(|r| r.date)
.min()
.ok_or_else(|| Error::DateNotFound("no data in obfr-2016.parquet".into()))
}
pub fn treasury_curve_blocking(&self, date: impl IntoDate) -> Result<YieldCurve> {
let date: Date = date.into_date()?;
block(self.treasury_par_curve(date))
}
pub fn treasury_range_blocking(
&self,
start: impl IntoDate,
end: impl IntoDate,
) -> Result<Vec<YieldCurve>> {
let start: Date = start.into_date()?;
let end: Date = end.into_date()?;
block(self.treasury_range(start, end))
}
pub fn treasury_rate_blocking(
&self,
date: impl IntoDate,
tenor: impl Into<Tenor>,
) -> Result<f64> {
let date: Date = date.into_date()?;
let tenor: Tenor = tenor.into();
block(self.treasury_rate(date, tenor))
}
pub fn treasury_latest_blocking(&self) -> Result<YieldCurve> {
block(self.treasury_latest())
}
pub fn sofr_blocking(&self, date: impl IntoDate) -> Result<f64> {
let date: Date = date.into_date()?;
block(self.sofr(date))
}
pub fn sofr_range_blocking(
&self,
start: impl IntoDate,
end: impl IntoDate,
) -> Result<Vec<SofrDay>> {
let start: Date = start.into_date()?;
let end: Date = end.into_date()?;
block(self.sofr_range(start, end))
}
pub fn sofr_latest_blocking(&self) -> Result<SofrDay> {
block(self.sofr_latest())
}
async fn treasury_year(&self, year: i32) -> Result<Vec<YieldCurve>> {
let key = format!("treasury-{year}");
let bytes = self.fetcher.fetch(&key).await?;
let tmp = tempfile_for_bytes(&bytes, &format!("{key}.parquet"))?;
let curves = read_treasury_year(tmp.path())?;
Ok(curves)
}
async fn sofr_year(&self, year: i32) -> Result<Vec<SofrDay>> {
let key = format!("sofr-{year}");
let bytes = self.fetcher.fetch(&key).await?;
let tmp = tempfile_for_bytes(&bytes, &format!("{key}.parquet"))?;
let rates = read_sofr_year(tmp.path())?;
Ok(rates)
}
async fn effr_year(&self, year: i32) -> Result<Vec<EffrDay>> {
let key = format!("effr-{year}");
let bytes = self.fetcher.fetch(&key).await?;
let tmp = tempfile_for_bytes(&bytes, &format!("{key}.parquet"))?;
let rates = read_effr_year(tmp.path())?;
Ok(rates)
}
async fn obfr_year(&self, year: i32) -> Result<Vec<ObfrDay>> {
let key = format!("obfr-{year}");
let bytes = self.fetcher.fetch(&key).await?;
let tmp = tempfile_for_bytes(&bytes, &format!("{key}.parquet"))?;
let rates = read_obfr_year(tmp.path())?;
Ok(rates)
}
}
impl Default for Curvekit {
fn default() -> Self {
Self::new()
}
}
fn block<F: std::future::Future<Output = Result<T>>, T>(fut: F) -> Result<T> {
match tokio::runtime::Handle::try_current() {
Ok(handle) => tokio::task::block_in_place(|| handle.block_on(fut)),
Err(_) => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(Error::Io)?;
rt.block_on(fut)
}
}
}
fn tempfile_for_bytes(bytes: &bytes::Bytes, _hint: &str) -> Result<tempfile::NamedTempFile> {
use std::io::Write;
let mut tmp = tempfile::NamedTempFile::new()?;
tmp.write_all(bytes)?;
tmp.flush()?;
Ok(tmp)
}