diff --git a/crates/fidc-core/src/corporate_book.rs b/crates/fidc-core/src/corporate_book.rs index 10615db..e2053b3 100644 --- a/crates/fidc-core/src/corporate_book.rs +++ b/crates/fidc-core/src/corporate_book.rs @@ -15,7 +15,7 @@ pub(crate) fn validate_action<'a>( action.symbol, action.date ) })?; - if action.share_cash.abs() > f64::EPSILON { + if action.share_cash != 0.0 { action.required_cash_payment_date()?; action.required_record_date()?; if action.share_cash < 0.0 { return Err("corporate_action_negative_cash_dividend".into()); } @@ -45,7 +45,7 @@ pub(crate) fn apply( let actions = data.corporate_actions_on(date); for action in actions { validate_action(action, data).map_err(BacktestError::Execution)?; - if cash_dividends_enabled && action.share_cash.abs() > f64::EPSILON { + if cash_dividends_enabled && action.share_cash != 0.0 { portfolio.registered_quantity(action.required_record_date().map_err(BacktestError::Execution)?, &action.symbol) .map_err(BacktestError::Execution)?; } @@ -55,7 +55,7 @@ pub(crate) fn apply( && portfolio .position(&action.symbol) .is_some_and(|position| position.quantity > 0) - || (cash_dividends_enabled && action.share_cash.abs() > f64::EPSILON + || (cash_dividends_enabled && action.share_cash != 0.0 && portfolio.registered_quantity(action.record_date.expect("validated record date"), &action.symbol).expect("validated closing fact") > 0) }) { return Ok(BrokerExecutionReport::default()); @@ -91,7 +91,7 @@ fn apply_inner( if !action.has_effect() { continue; } - if cash_dividends_enabled && action.share_cash.abs() > f64::EPSILON { + if cash_dividends_enabled && action.share_cash != 0.0 { let record_date = action.required_record_date().map_err(BacktestError::Execution)?; let entitled_quantity = portfolio.registered_quantity(record_date, &action.symbol).map_err(BacktestError::Execution)?; let cash_before = portfolio.cash(); @@ -757,6 +757,28 @@ mod tests { } } + #[test] + fn tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence() { + let rate = 1e-16; + for payment in [None, Some(date())] { + let mut action = dividend(payment); + action.share_cash = rate; + assert!(action.has_effect()); + let data = conversion_data(vec![action], false); + if payment.is_none() { + assert!(data.get_dividend("000001.SZ", date(), date()).is_err()); + let mut book = conversion_book(); + let before = book.financial_replay_identity(); + assert!(apply(date(), &data, &mut book, &mut Vec::new(), true, true).is_err()); + assert_eq!(book.financial_replay_identity(), before); + } else { + let observed = data.get_dividend("000001.SZ", date(), date()).unwrap(); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].dividend_cash_before_tax, rate); + } + } + } + #[test] fn explicitly_delayed_dividend_remains_receivable_until_actual_payment() { let payment = date().succ_opt().unwrap().succ_opt().unwrap(); diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index e6b21b4..7f07e52 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -468,7 +468,7 @@ impl CorporateAction { } pub fn has_effect(&self) -> bool { - self.share_cash.abs() > f64::EPSILON + self.share_cash != 0.0 || (self.split_ratio() - 1.0).abs() > f64::EPSILON || self.issue_quantity.abs() > f64::EPSILON || self.reform @@ -2909,7 +2909,7 @@ impl DataSet { .corporate_actions_by_date .range(start..=end) .flat_map(|(_, actions)| actions.iter()) - .filter(|action| action.symbol == symbol && (!action.share_cash.is_finite() || action.share_cash.abs() > f64::EPSILON)) + .filter(|action| action.symbol == symbol && action.share_cash != 0.0) .map(|action| { crate::finite_serialization::validate(action).map_err(|error| BacktestError::Execution(format!( "corporate_action_invalid_number: symbol={} ex_date={} detail={error}", action.symbol, action.date)))?;