保留极小非零股息并执行相同权益日期门禁
This commit is contained in:
@@ -15,7 +15,7 @@ pub(crate) fn validate_action<'a>(
|
|||||||
action.symbol, action.date
|
action.symbol, action.date
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if action.share_cash.abs() > f64::EPSILON {
|
if action.share_cash != 0.0 {
|
||||||
action.required_cash_payment_date()?;
|
action.required_cash_payment_date()?;
|
||||||
action.required_record_date()?;
|
action.required_record_date()?;
|
||||||
if action.share_cash < 0.0 { return Err("corporate_action_negative_cash_dividend".into()); }
|
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);
|
let actions = data.corporate_actions_on(date);
|
||||||
for action in actions {
|
for action in actions {
|
||||||
validate_action(action, data).map_err(BacktestError::Execution)?;
|
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)
|
portfolio.registered_quantity(action.required_record_date().map_err(BacktestError::Execution)?, &action.symbol)
|
||||||
.map_err(BacktestError::Execution)?;
|
.map_err(BacktestError::Execution)?;
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ pub(crate) fn apply(
|
|||||||
&& portfolio
|
&& portfolio
|
||||||
.position(&action.symbol)
|
.position(&action.symbol)
|
||||||
.is_some_and(|position| position.quantity > 0)
|
.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)
|
&& portfolio.registered_quantity(action.record_date.expect("validated record date"), &action.symbol).expect("validated closing fact") > 0)
|
||||||
}) {
|
}) {
|
||||||
return Ok(BrokerExecutionReport::default());
|
return Ok(BrokerExecutionReport::default());
|
||||||
@@ -91,7 +91,7 @@ fn apply_inner(
|
|||||||
if !action.has_effect() {
|
if !action.has_effect() {
|
||||||
continue;
|
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 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 entitled_quantity = portfolio.registered_quantity(record_date, &action.symbol).map_err(BacktestError::Execution)?;
|
||||||
let cash_before = portfolio.cash();
|
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]
|
#[test]
|
||||||
fn explicitly_delayed_dividend_remains_receivable_until_actual_payment() {
|
fn explicitly_delayed_dividend_remains_receivable_until_actual_payment() {
|
||||||
let payment = date().succ_opt().unwrap().succ_opt().unwrap();
|
let payment = date().succ_opt().unwrap().succ_opt().unwrap();
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ impl CorporateAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_effect(&self) -> bool {
|
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.split_ratio() - 1.0).abs() > f64::EPSILON
|
||||||
|| self.issue_quantity.abs() > f64::EPSILON
|
|| self.issue_quantity.abs() > f64::EPSILON
|
||||||
|| self.reform
|
|| self.reform
|
||||||
@@ -2909,7 +2909,7 @@ impl DataSet {
|
|||||||
.corporate_actions_by_date
|
.corporate_actions_by_date
|
||||||
.range(start..=end)
|
.range(start..=end)
|
||||||
.flat_map(|(_, actions)| actions.iter())
|
.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| {
|
.map(|action| {
|
||||||
crate::finite_serialization::validate(action).map_err(|error| BacktestError::Execution(format!(
|
crate::finite_serialization::validate(action).map_err(|error| BacktestError::Execution(format!(
|
||||||
"corporate_action_invalid_number: symbol={} ex_date={} detail={error}", action.symbol, action.date)))?;
|
"corporate_action_invalid_number: symbol={} ex_date={} detail={error}", action.symbol, action.date)))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user