diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index fd30cfc..24ec5d3 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -3242,14 +3242,16 @@ where notes: &mut Vec, ) -> Result { let mut report = BrokerExecutionReport::default(); - let settled = portfolio.settle_cash_receivables(date); - for receivable in settled { + let due = portfolio.take_due_cash_receivables(date); + for receivable in due { + let cash_before = portfolio.cash(); + portfolio + .settle_cash_receivable(&receivable) + .map_err(BacktestError::Execution)?; let mut note = format!( "cash_receivable_settled {} ex_date={} payable_date={} cash={:.2}", receivable.symbol, receivable.ex_date, receivable.payable_date, receivable.amount ); - let cash_before = portfolio.cash() - receivable.amount; - if self.dividend_reinvestment && receivable.reason.starts_with("cash_dividend") && receivable.amount > 0.0 diff --git a/crates/fidc-core/src/portfolio.rs b/crates/fidc-core/src/portfolio.rs index 39203ff..a36447f 100644 --- a/crates/fidc-core/src/portfolio.rs +++ b/crates/fidc-core/src/portfolio.rs @@ -778,25 +778,23 @@ impl PortfolioState { Ok(()) } - pub fn settle_cash_receivables(&mut self, date: NaiveDate) -> Vec { - let mut settled = Vec::new(); + pub fn take_due_cash_receivables(&mut self, date: NaiveDate) -> Vec { + let mut due = Vec::new(); let mut pending = Vec::new(); for receivable in self.cash_receivables.drain(..) { if receivable.payable_date <= date { - let amount = Self::fixed_money(receivable.amount, "cash receivable") - .expect("cash receivable must be finite fixed-point money"); - self.cash = self - .cash - .checked_add(amount) - .expect("fixed-point cash overflow while settling receivable"); - settled.push(receivable); + due.push(receivable); } else { pending.push(receivable); } } self.cash_receivables = pending; self.refresh_dividend_receivables(); - settled + due + } + + pub fn settle_cash_receivable(&mut self, receivable: &CashReceivable) -> Result<(), String> { + self.apply_cash_delta(receivable.amount) } pub fn cash_receivables(&self) -> &[CashReceivable] { diff --git a/crates/fidc-core/tests/corporate_actions.rs b/crates/fidc-core/tests/corporate_actions.rs index 0e34073..88673a7 100644 --- a/crates/fidc-core/tests/corporate_actions.rs +++ b/crates/fidc-core/tests/corporate_actions.rs @@ -49,14 +49,30 @@ fn portfolio_settles_cash_receivable_on_payable_date() { amount: 500.0, reason: "cash_dividend 0.5".to_string(), }); + portfolio.add_cash_receivable(CashReceivable { + symbol: "000002.SZ".to_string(), + ex_date: d(2025, 1, 2), + payable_date: d(2025, 1, 5), + amount: 250.0, + reason: "cash_dividend 0.25".to_string(), + }); - let settled_early = portfolio.settle_cash_receivables(d(2025, 1, 4)); - assert!(settled_early.is_empty()); + let due_early = portfolio.take_due_cash_receivables(d(2025, 1, 4)); + assert!(due_early.is_empty()); assert!((portfolio.cash() - 1_000_000.0).abs() < 1e-9); - let settled = portfolio.settle_cash_receivables(d(2025, 1, 5)); - assert_eq!(settled.len(), 1); - assert!((portfolio.cash() - 1_000_500.0).abs() < 1e-9); + let due = portfolio.take_due_cash_receivables(d(2025, 1, 5)); + assert_eq!(due.len(), 2); + let mut cash_chain = Vec::new(); + for receivable in &due { + let cash_before = portfolio.cash(); + portfolio.settle_cash_receivable(receivable).unwrap(); + cash_chain.push((cash_before, portfolio.cash())); + } + assert_eq!( + cash_chain, + vec![(1_000_000.0, 1_000_500.0), (1_000_500.0, 1_000_750.0)] + ); assert!(portfolio.cash_receivables().is_empty()); }