Add account cash flow intents

This commit is contained in:
boris
2026-04-23 20:14:05 -07:00
parent e0a5d0c945
commit 85feee6dac
10 changed files with 608 additions and 27 deletions

View File

@@ -308,9 +308,19 @@ impl Position {
#[derive(Debug, Clone)]
pub struct PortfolioState {
initial_cash: f64,
units: f64,
cash: f64,
cash_liabilities: f64,
positions: IndexMap<String, Position>,
cash_receivables: Vec<CashReceivable>,
pending_cash_flows: Vec<PendingCashFlow>,
}
#[derive(Debug, Clone)]
pub struct PendingCashFlow {
pub payable_date: NaiveDate,
pub amount: f64,
pub reason: String,
}
#[derive(Debug, Clone)]
@@ -328,24 +338,35 @@ impl PortfolioState {
pub fn new(initial_cash: f64) -> Self {
Self {
initial_cash,
units: initial_cash,
cash: initial_cash,
cash_liabilities: 0.0,
positions: IndexMap::new(),
cash_receivables: Vec::new(),
pending_cash_flows: Vec::new(),
}
}
pub fn starting_cash(&self) -> f64 {
self.units
}
pub fn initial_cash(&self) -> f64 {
self.initial_cash
}
pub fn units(&self) -> f64 {
self.initial_cash
self.units
}
pub fn cash(&self) -> f64 {
self.cash
}
pub fn cash_liabilities(&self) -> f64 {
self.cash_liabilities
}
pub fn positions(&self) -> &IndexMap<String, Position> {
&self.positions
}
@@ -377,6 +398,92 @@ impl PortfolioState {
self.refresh_dividend_receivables();
}
pub fn deposit_withdraw(&mut self, amount: f64) -> Result<(), String> {
if !amount.is_finite() {
return Err("deposit_withdraw amount must be finite".to_string());
}
if amount < 0.0 && self.cash + amount < -1e-6 {
return Err(format!(
"insufficient cash for withdrawal amount={:.2} cash={:.2}",
amount, self.cash
));
}
let unit_net_value = self.unit_net_value();
self.cash += amount;
self.rebase_units_after_external_cash_flow(unit_net_value);
Ok(())
}
pub fn schedule_deposit_withdraw(
&mut self,
payable_date: NaiveDate,
amount: f64,
reason: impl Into<String>,
) -> Result<(), String> {
if !amount.is_finite() {
return Err("deposit_withdraw amount must be finite".to_string());
}
if amount < 0.0 && self.cash + amount < -1e-6 {
return Err(format!(
"insufficient cash for scheduled withdrawal amount={:.2} cash={:.2}",
amount, self.cash
));
}
self.pending_cash_flows.push(PendingCashFlow {
payable_date,
amount,
reason: reason.into(),
});
self.pending_cash_flows
.sort_by_key(|flow| flow.payable_date);
Ok(())
}
pub fn settle_pending_cash_flows(&mut self, date: NaiveDate) -> Vec<PendingCashFlow> {
let mut settled = Vec::new();
let mut pending = Vec::new();
for flow in std::mem::take(&mut self.pending_cash_flows) {
if flow.payable_date <= date {
let unit_net_value = self.unit_net_value();
self.cash += flow.amount;
self.rebase_units_after_external_cash_flow(unit_net_value);
settled.push(flow);
} else {
pending.push(flow);
}
}
self.pending_cash_flows = pending;
settled
}
pub fn pending_cash_flows(&self) -> &[PendingCashFlow] {
&self.pending_cash_flows
}
pub fn finance_repay(&mut self, amount: f64) -> Result<(), String> {
if !amount.is_finite() {
return Err("finance_repay amount must be finite".to_string());
}
if amount > 0.0 {
self.cash_liabilities += amount;
self.cash += amount;
return Ok(());
}
if amount < 0.0 {
let repay_amount = (-amount).min(self.cash_liabilities);
if repay_amount > self.cash + 1e-6 {
return Err(format!(
"insufficient cash for finance repay amount={:.2} cash={:.2}",
repay_amount, self.cash
));
}
self.cash_liabilities -= repay_amount;
self.cash -= repay_amount;
}
Ok(())
}
pub fn settle_cash_receivables(&mut self, date: NaiveDate) -> Vec<CashReceivable> {
let mut settled = Vec::new();
let mut pending = Vec::new();
@@ -459,7 +566,7 @@ impl PortfolioState {
}
pub fn total_equity(&self) -> f64 {
self.cash + self.market_value()
self.cash + self.market_value() - self.cash_liabilities
}
pub fn total_value(&self) -> f64 {
@@ -471,18 +578,18 @@ impl PortfolioState {
}
pub fn unit_net_value(&self) -> f64 {
if self.initial_cash.abs() < f64::EPSILON {
if self.units.abs() < f64::EPSILON {
0.0
} else {
self.total_equity() / self.initial_cash
self.total_equity() / self.units
}
}
pub fn static_unit_net_value(&self) -> f64 {
if self.initial_cash.abs() < f64::EPSILON {
if self.units.abs() < f64::EPSILON {
0.0
} else {
(self.total_equity() - self.daily_pnl()) / self.initial_cash
(self.total_equity() - self.daily_pnl()) / self.units
}
}
@@ -619,6 +726,12 @@ impl PortfolioState {
position.set_dividend_receivable(per_symbol.get(symbol).copied().unwrap_or(0.0));
}
}
fn rebase_units_after_external_cash_flow(&mut self, unit_net_value_before: f64) {
if unit_net_value_before > 0.0 && unit_net_value_before.is_finite() {
self.units = self.total_equity() / unit_net_value_before;
}
}
}
#[cfg(test)]