use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, HashMap};
use crate::math::comparison::close;
use crate::patterns::observable::{Observable, Observer};
use crate::require;
use crate::shared::{Shared, SharedMut};
use crate::time::date::Date;
use crate::types::Rate;
pub struct Settings<D> {
evaluation_date: Cell<Option<D>>,
eval_date_observable: Observable,
include_reference_date_events: Cell<bool>,
include_todays_cash_flows: Cell<Option<bool>>,
enforces_todays_historic_fixings: Cell<bool>,
using_at_par_coupons: Cell<bool>,
fixing_store: RefCell<HashMap<String, BTreeMap<Date, Rate>>>,
fixing_notifiers: RefCell<HashMap<String, Shared<Observable>>>,
}
impl<D> Default for Settings<D> {
fn default() -> Self {
Settings {
evaluation_date: Cell::new(None),
eval_date_observable: Observable::new(),
include_reference_date_events: Cell::new(false),
include_todays_cash_flows: Cell::new(None),
enforces_todays_historic_fixings: Cell::new(false),
using_at_par_coupons: Cell::new(true),
fixing_store: RefCell::new(HashMap::new()),
fixing_notifiers: RefCell::new(HashMap::new()),
}
}
}
impl<D> Settings<D> {
pub fn new() -> Self {
Settings::default()
}
pub fn evaluation_date(&self) -> Option<D>
where
D: Copy,
{
self.evaluation_date.get()
}
pub fn set_evaluation_date(&self, date: D)
where
D: Copy + PartialEq,
{
if self.evaluation_date.get() != Some(date) {
self.evaluation_date.set(Some(date));
self.eval_date_observable.notify_observers();
}
}
pub fn reset_evaluation_date(&self) {
if self.evaluation_date.replace(None).is_some() {
self.eval_date_observable.notify_observers();
}
}
pub fn register_eval_date_observer(&self, observer: &SharedMut<dyn Observer>) -> bool {
self.eval_date_observable.register_observer(observer)
}
pub fn include_reference_date_events(&self) -> bool {
self.include_reference_date_events.get()
}
pub fn set_include_reference_date_events(&self, value: bool) {
self.include_reference_date_events.set(value);
}
pub fn include_todays_cash_flows(&self) -> Option<bool> {
self.include_todays_cash_flows.get()
}
pub fn set_include_todays_cash_flows(&self, value: Option<bool>) {
self.include_todays_cash_flows.set(value);
}
pub fn enforces_todays_historic_fixings(&self) -> bool {
self.enforces_todays_historic_fixings.get()
}
pub fn set_enforces_todays_historic_fixings(&self, value: bool) {
self.enforces_todays_historic_fixings.set(value);
}
pub fn using_at_par_coupons(&self) -> bool {
self.using_at_par_coupons.get()
}
pub fn set_using_at_par_coupons(&self, value: bool) {
self.using_at_par_coupons.set(value);
}
fn fixing_key(index_name: &str) -> String {
index_name.to_uppercase()
}
fn fixing_notifier(&self, key: &str) -> Shared<Observable> {
let mut notifiers = self.fixing_notifiers.borrow_mut();
if let Some(notifier) = notifiers.get(key) {
return Shared::clone(notifier);
}
let notifier = Shared::new(Observable::new());
notifiers.insert(key.to_string(), Shared::clone(¬ifier));
notifier
}
pub fn register_fixing_observer(
&self,
index_name: &str,
observer: &SharedMut<dyn Observer>,
) -> bool {
let key = Self::fixing_key(index_name);
self.fixing_notifier(&key).register_observer(observer)
}
pub fn add_fixing(
&self,
index_name: &str,
date: Date,
rate: Rate,
) -> crate::errors::QlResult<()> {
self.add_fixings(index_name, [(date, rate)])
}
pub fn add_fixings(
&self,
index_name: &str,
fixings: impl IntoIterator<Item = (Date, Rate)>,
) -> crate::errors::QlResult<()> {
let key = Self::fixing_key(index_name);
let notifier = self.fixing_notifier(&key);
{
let mut store = self.fixing_store.borrow_mut();
let history = store.entry(key).or_default();
let pending: Vec<(Date, Rate)> = fixings.into_iter().collect();
for &(date, rate) in &pending {
if let Some(&existing) = history.get(&date) {
require!(
close(existing, rate),
"duplicated fixing {rate} on {date:?} while {existing} is already present"
);
}
}
for (date, rate) in pending {
history.insert(date, rate);
}
}
notifier.notify_observers();
Ok(())
}
pub fn fixing(&self, index_name: &str, date: Date) -> Option<Rate> {
let key = Self::fixing_key(index_name);
self.fixing_store
.borrow()
.get(&key)
.and_then(|history| history.get(&date).copied())
}
pub fn has_historical_fixing(&self, index_name: &str, date: Date) -> bool {
self.fixing(index_name, date).is_some()
}
pub fn clear_fixing(&self, index_name: &str) {
let key = Self::fixing_key(index_name);
self.fixing_notifier(&key).notify_observers();
self.fixing_store.borrow_mut().remove(&key);
}
pub fn clear_fixings(&self) {
let names: Vec<String> = self.fixing_store.borrow().keys().cloned().collect();
for name in &names {
self.fixing_notifier(name).notify_observers();
}
self.fixing_store.borrow_mut().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
#[derive(Default)]
struct Flag {
up: bool,
}
impl Observer for Flag {
fn update(&mut self) {
self.up = true;
}
}
#[test]
fn notifies_only_on_actual_date_change() {
let settings: Settings<i64> = Settings::new();
let d1 = 44238_i64; let d2 = 44239_i64;
settings.set_evaluation_date(d1);
let flag = shared_mut(Flag::default());
settings.register_eval_date_observer(&(flag.clone() as SharedMut<dyn Observer>));
settings.set_evaluation_date(d1);
assert!(!flag.borrow().up);
settings.set_evaluation_date(d2);
assert!(flag.borrow().up);
assert_eq!(settings.evaluation_date(), Some(d2));
}
#[test]
fn reset_returns_to_floating_and_notifies_once() {
let settings: Settings<i64> = Settings::new();
settings.set_evaluation_date(44238_i64);
let flag = shared_mut(Flag::default());
settings.register_eval_date_observer(&(flag.clone() as SharedMut<dyn Observer>));
settings.reset_evaluation_date();
assert!(flag.borrow().up);
assert_eq!(settings.evaluation_date(), None);
flag.borrow_mut().up = false;
settings.reset_evaluation_date();
assert!(!flag.borrow().up);
}
#[test]
fn flags_round_trip() {
let settings: Settings<i64> = Settings::new();
assert!(!settings.include_reference_date_events());
assert_eq!(settings.include_todays_cash_flows(), None);
assert!(!settings.enforces_todays_historic_fixings());
settings.set_include_reference_date_events(true);
settings.set_include_todays_cash_flows(Some(true));
settings.set_enforces_todays_historic_fixings(true);
assert!(settings.include_reference_date_events());
assert_eq!(settings.include_todays_cash_flows(), Some(true));
assert!(settings.enforces_todays_historic_fixings());
}
struct Reader {
settings: Shared<Settings<i64>>,
seen: Option<i64>,
}
impl Observer for Reader {
fn update(&mut self) {
self.seen = self.settings.evaluation_date();
}
}
#[test]
fn observer_may_read_the_evaluation_date_during_notification() {
let settings = shared(Settings::<i64>::new());
let reader = shared_mut(Reader {
settings: Shared::clone(&settings),
seen: None,
});
settings.register_eval_date_observer(&(reader.clone() as SharedMut<dyn Observer>));
settings.set_evaluation_date(44239_i64);
assert_eq!(reader.borrow().seen, Some(44239));
settings.reset_evaluation_date();
assert_eq!(reader.borrow().seen, None);
}
use crate::time::date::Month;
fn a_date() -> Date {
Date::new(15, Month::June, 2026)
}
#[test]
fn adding_a_fixing_notifies_observers_of_that_index_only() {
let settings: Settings<i64> = Settings::new();
let date = a_date();
let f1 = shared_mut(Flag::default());
settings.register_fixing_observer("Euribor6M", &(f1.clone() as SharedMut<dyn Observer>));
let f2 = shared_mut(Flag::default());
settings.register_fixing_observer("BMA", &(f2.clone() as SharedMut<dyn Observer>));
settings.add_fixing("Euribor6M", date, -0.003).unwrap();
assert!(f1.borrow().up);
assert!(!f2.borrow().up);
settings.add_fixing("BMA", date, 0.01).unwrap();
assert!(f2.borrow().up);
}
#[test]
fn has_historical_fixing_tracks_registered_fixings() {
let settings: Settings<i64> = Settings::new();
let date = a_date();
settings.add_fixing("Euribor6M", date, 0.01).unwrap();
assert!(!settings.has_historical_fixing("Euribor3M", date));
assert!(settings.has_historical_fixing("Euribor6M", date));
assert_eq!(settings.fixing("Euribor6M", date), Some(0.01));
settings.clear_fixings();
assert!(!settings.has_historical_fixing("Euribor3M", date));
assert!(!settings.has_historical_fixing("Euribor6M", date));
}
#[test]
fn index_names_are_case_insensitive() {
let settings: Settings<i64> = Settings::new();
let date = a_date();
settings.add_fixing("Euribor6M", date, 0.02).unwrap();
assert_eq!(settings.fixing("EURIBOR6M", date), Some(0.02));
assert!(settings.has_historical_fixing("euribor6m", date));
}
#[test]
fn conflicting_fixing_is_rejected_while_identical_is_idempotent() {
let settings: Settings<i64> = Settings::new();
let date = a_date();
settings.add_fixing("Euribor6M", date, 0.02).unwrap();
assert!(settings.add_fixing("Euribor6M", date, 0.02).is_ok());
assert!(settings.add_fixing("Euribor6M", date, 0.03).is_err());
assert_eq!(settings.fixing("Euribor6M", date), Some(0.02));
}
#[test]
fn fixing_observers_persist_across_clear() {
let settings: Settings<i64> = Settings::new();
let date = a_date();
let flag = shared_mut(Flag::default());
settings.register_fixing_observer("Euribor6M", &(flag.clone() as SharedMut<dyn Observer>));
settings.add_fixing("Euribor6M", date, 0.01).unwrap();
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
settings.clear_fixings();
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
settings.add_fixing("Euribor6M", date, 0.02).unwrap();
assert!(flag.borrow().up);
}
struct FixingReader {
settings: Shared<Settings<i64>>,
date: Date,
seen: Option<Rate>,
}
impl Observer for FixingReader {
fn update(&mut self) {
self.seen = self.settings.fixing("Euribor6M", self.date);
}
}
#[test]
fn observer_may_read_the_fixing_during_notification() {
let settings = shared(Settings::<i64>::new());
let date = a_date();
let reader = shared_mut(FixingReader {
settings: Shared::clone(&settings),
date,
seen: None,
});
settings
.register_fixing_observer("Euribor6M", &(reader.clone() as SharedMut<dyn Observer>));
settings.add_fixing("Euribor6M", date, 0.05).unwrap();
assert_eq!(reader.borrow().seen, Some(0.05));
}
}