use std::collections::BTreeMap;
use std::fmt::Display;
use chrono::NaiveDate;
use crate::traits::FloatExt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PriceIndex {
CpiUsAllItems,
RpiUk,
CpiUk,
HicpExTobacco,
Custom(String),
}
impl PriceIndex {
pub fn ticker(&self) -> &str {
match self {
Self::CpiUsAllItems => "CPI-U",
Self::RpiUk => "UKRPI",
Self::CpiUk => "UKCPI",
Self::HicpExTobacco => "HICPx",
Self::Custom(name) => name.as_str(),
}
}
pub fn description(&self) -> &str {
match self {
Self::CpiUsAllItems => "US Consumer Price Index, All Urban Consumers",
Self::RpiUk => "UK Retail Price Index",
Self::CpiUk => "UK Consumer Price Index",
Self::HicpExTobacco => "Eurozone HICP excluding tobacco",
Self::Custom(_) => "User-defined price index",
}
}
pub fn publication_lag_months(&self) -> u32 {
match self {
Self::CpiUsAllItems => 1,
Self::RpiUk | Self::CpiUk => 1,
Self::HicpExTobacco => 1,
Self::Custom(_) => 1,
}
}
}
impl Display for PriceIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.ticker())
}
}
#[derive(Debug, Clone, Default)]
pub struct FixingHistory<T: FloatExt> {
fixings: BTreeMap<NaiveDate, T>,
}
impl<T: FloatExt> FixingHistory<T> {
pub fn new() -> Self {
Self {
fixings: BTreeMap::new(),
}
}
pub fn add(&mut self, date: NaiveDate, value: T) {
self.fixings.insert(date, value);
}
pub fn get(&self, date: NaiveDate) -> Option<T> {
self.fixings.get(&date).copied()
}
pub fn last_before(&self, date: NaiveDate) -> Option<(NaiveDate, T)> {
self
.fixings
.range(..=date)
.next_back()
.map(|(d, v)| (*d, *v))
}
pub fn len(&self) -> usize {
self.fixings.len()
}
pub fn is_empty(&self) -> bool {
self.fixings.is_empty()
}
pub fn reference_ratio(&self, date: NaiveDate, base_value: T) -> Option<T> {
let (prev_date, prev_val) = self.last_before(date)?;
if prev_date == date {
return Some(prev_val / base_value);
}
let next = self.fixings.range(date..).next().map(|(d, v)| (*d, *v))?;
if next.0 == date {
return Some(next.1 / base_value);
}
let prev_days = (date - prev_date).num_days() as f64;
let total_days = (next.0 - prev_date).num_days() as f64;
let frac = T::from_f64_fast(prev_days / total_days);
let interp = prev_val + (next.1 - prev_val) * frac;
Some(interp / base_value)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}
#[test]
fn ticker_and_description() {
assert_eq!(PriceIndex::CpiUsAllItems.ticker(), "CPI-U");
assert_eq!(PriceIndex::HicpExTobacco.ticker(), "HICPx");
let custom = PriceIndex::Custom("MY-INDEX".to_string());
assert_eq!(custom.ticker(), "MY-INDEX");
}
#[test]
fn fixing_history_lookup() {
let mut h: FixingHistory<f64> = FixingHistory::new();
h.add(d(2025, 1, 1), 300.0);
h.add(d(2025, 2, 1), 301.5);
assert_eq!(h.get(d(2025, 1, 1)), Some(300.0));
assert_eq!(h.get(d(2025, 1, 15)), None);
let (last_d, last_v) = h.last_before(d(2025, 1, 20)).unwrap();
assert_eq!(last_d, d(2025, 1, 1));
assert_eq!(last_v, 300.0);
}
#[test]
fn reference_ratio_interpolates_linearly() {
let mut h: FixingHistory<f64> = FixingHistory::new();
h.add(d(2025, 1, 1), 300.0);
h.add(d(2025, 2, 1), 303.0);
let base = 300.0;
let ratio = h.reference_ratio(d(2025, 1, 16), base).unwrap();
let expected_index = 300.0 + 3.0 * 15.0 / 31.0;
assert!((ratio - expected_index / base).abs() < 1e-12);
}
#[test]
fn reference_ratio_no_extrapolation() {
let mut h: FixingHistory<f64> = FixingHistory::new();
h.add(d(2025, 1, 1), 300.0);
assert!(h.reference_ratio(d(2025, 2, 1), 300.0).is_none());
}
}