保留极小非零股息并执行相同权益日期门禁

This commit is contained in:
boris
2026-10-05 02:55:17 +08:00
parent af655de0c1
commit d3f49a17ef
2 changed files with 28 additions and 6 deletions
+26 -4
View File
@@ -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();
+2 -2
View File
@@ -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)))?;