Add account cash flow intents
This commit is contained in:
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user