按顺序结算多笔现金应收

This commit is contained in:
boris
2026-08-25 15:25:18 +08:00
parent e368bad7e4
commit 2574b9375d
3 changed files with 35 additions and 19 deletions
+6 -4
View File
@@ -3242,14 +3242,16 @@ where
notes: &mut Vec<String>, notes: &mut Vec<String>,
) -> Result<BrokerExecutionReport, BacktestError> { ) -> Result<BrokerExecutionReport, BacktestError> {
let mut report = BrokerExecutionReport::default(); let mut report = BrokerExecutionReport::default();
let settled = portfolio.settle_cash_receivables(date); let due = portfolio.take_due_cash_receivables(date);
for receivable in settled { for receivable in due {
let cash_before = portfolio.cash();
portfolio
.settle_cash_receivable(&receivable)
.map_err(BacktestError::Execution)?;
let mut note = format!( let mut note = format!(
"cash_receivable_settled {} ex_date={} payable_date={} cash={:.2}", "cash_receivable_settled {} ex_date={} payable_date={} cash={:.2}",
receivable.symbol, receivable.ex_date, receivable.payable_date, receivable.amount receivable.symbol, receivable.ex_date, receivable.payable_date, receivable.amount
); );
let cash_before = portfolio.cash() - receivable.amount;
if self.dividend_reinvestment if self.dividend_reinvestment
&& receivable.reason.starts_with("cash_dividend") && receivable.reason.starts_with("cash_dividend")
&& receivable.amount > 0.0 && receivable.amount > 0.0
+8 -10
View File
@@ -778,25 +778,23 @@ impl PortfolioState {
Ok(()) Ok(())
} }
pub fn settle_cash_receivables(&mut self, date: NaiveDate) -> Vec<CashReceivable> { pub fn take_due_cash_receivables(&mut self, date: NaiveDate) -> Vec<CashReceivable> {
let mut settled = Vec::new(); let mut due = Vec::new();
let mut pending = Vec::new(); let mut pending = Vec::new();
for receivable in self.cash_receivables.drain(..) { for receivable in self.cash_receivables.drain(..) {
if receivable.payable_date <= date { if receivable.payable_date <= date {
let amount = Self::fixed_money(receivable.amount, "cash receivable") due.push(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);
} else { } else {
pending.push(receivable); pending.push(receivable);
} }
} }
self.cash_receivables = pending; self.cash_receivables = pending;
self.refresh_dividend_receivables(); 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] { pub fn cash_receivables(&self) -> &[CashReceivable] {
+21 -5
View File
@@ -49,14 +49,30 @@ fn portfolio_settles_cash_receivable_on_payable_date() {
amount: 500.0, amount: 500.0,
reason: "cash_dividend 0.5".to_string(), 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)); let due_early = portfolio.take_due_cash_receivables(d(2025, 1, 4));
assert!(settled_early.is_empty()); assert!(due_early.is_empty());
assert!((portfolio.cash() - 1_000_000.0).abs() < 1e-9); assert!((portfolio.cash() - 1_000_000.0).abs() < 1e-9);
let settled = portfolio.settle_cash_receivables(d(2025, 1, 5)); let due = portfolio.take_due_cash_receivables(d(2025, 1, 5));
assert_eq!(settled.len(), 1); assert_eq!(due.len(), 2);
assert!((portfolio.cash() - 1_000_500.0).abs() < 1e-9); 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()); assert!(portfolio.cash_receivables().is_empty());
} }