diff --git a/crates/fidc-core/src/corporate_book.rs b/crates/fidc-core/src/corporate_book.rs index 6a5ddb3..10615db 100644 --- a/crates/fidc-core/src/corporate_book.rs +++ b/crates/fidc-core/src/corporate_book.rs @@ -765,16 +765,38 @@ mod tests { assert_eq!(records[0].payable_date, payment); let mut book = conversion_book(); let mut notes = Vec::new(); + let equity_before = book.total_equity(); apply(date(), &data, &mut book, &mut notes, true, true).unwrap(); assert_eq!(book.cash(), 1000.); assert_eq!(book.cash_receivables().len(), 1); assert_eq!(book.cash_receivables()[0].amount, 50.); + assert_eq!(book.cash_receivable_value(), 50.); + assert_eq!(book.total_equity(), equity_before); + assert_eq!(book.unit_net_value(), equity_before / book.units()); settle_receivables(date().succ_opt().unwrap(), &data, &mut book, &mut notes, false, None).unwrap(); assert_eq!(book.cash(), 1000.); assert_eq!(book.cash_receivables().len(), 1); + assert_eq!(book.total_equity(), equity_before); settle_receivables(payment, &data, &mut book, &mut notes, false, None).unwrap(); assert_eq!(book.cash(), 1050.); assert!(book.cash_receivables().is_empty()); + assert_eq!(book.cash_receivable_value(), 0.); + assert_eq!(book.total_equity(), equity_before); + } + + #[test] + fn dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash() { + let mut account = PortfolioState::new(1000.); + account.add_cash_receivable(CashReceivable { symbol: "000001.SZ".into(), + ex_date: date(), payable_date: date().succ_opt().unwrap(), amount: 50., reason: "registered dividend".into() }); + account.prune_flat_positions(); + assert_eq!(account.market_value(), 0.); + assert_eq!(account.cash(), 1000.); + assert_eq!(account.total_equity(), 1050.); + assert!(account.deposit_withdraw(-1001.).is_err()); + settle_receivables(date().succ_opt().unwrap(), &data(false), &mut account, &mut Vec::new(), false, None).unwrap(); + assert_eq!(account.cash(), 1050.); + assert_eq!(account.total_equity(), 1050.); } #[test] diff --git a/crates/fidc-core/src/portfolio.rs b/crates/fidc-core/src/portfolio.rs index f301dfb..e41e45d 100644 --- a/crates/fidc-core/src/portfolio.rs +++ b/crates/fidc-core/src/portfolio.rs @@ -1485,6 +1485,16 @@ impl PortfolioState { &self.cash_receivables } + pub fn cash_receivable_value(&self) -> f64 { + self.cash_receivables.iter().try_fold(FixedMoney::ZERO, |total, receivable| { + let amount = fixed_money(receivable.amount, "cash receivable asset")?; + if amount < FixedMoney::ZERO { + return Err("cash receivable asset cannot be negative".to_string()); + } + total.checked_add(amount).ok_or_else(|| "cash receivable asset overflow".to_string()) + }).expect("cash receivable asset must be valid fixed-point money").to_f64() + } + pub fn begin_trading_day(&mut self) { self.day_sold_symbols.clear(); for position in self.positions.values_mut() { @@ -1589,7 +1599,8 @@ impl PortfolioState { } pub fn total_equity(&self) -> f64 { - self.cash.to_f64() + self.market_value() - self.cash_liabilities.to_f64() + self.cash.to_f64() + self.market_value() + self.cash_receivable_value() + - self.cash_liabilities.to_f64() } pub fn total_value(&self) -> f64 {