修复迟到成交跨公司行为的经济账本校正

This commit is contained in:
boris
2026-09-14 21:14:20 +08:00
parent ef9cc39882
commit 05f1cbbe00
11 changed files with 1447 additions and 154 deletions
+51 -1
View File
@@ -734,6 +734,10 @@ impl PortfolioState {
pub fn new(initial_cash: f64) -> Self {
let initial_cash = fixed_money(initial_cash, "initial cash")
.expect("initial cash must be finite fixed-point money");
Self::from_fixed_initial_cash(initial_cash)
}
pub(crate) fn from_fixed_initial_cash(initial_cash: FixedMoney) -> Self {
Self {
initial_cash,
units: initial_cash,
@@ -756,6 +760,8 @@ impl PortfolioState {
self.initial_cash.to_f64()
}
pub(crate) fn initial_cash_fixed(&self) -> FixedMoney { self.initial_cash }
pub(crate) fn stock_pool_execution_state(&self,pool_id:&str)->crate::stock_pool_state::StockPoolExecutionState{
self.stock_pool_states.get(pool_id).cloned().unwrap_or_default()
}
@@ -787,6 +793,46 @@ impl PortfolioState {
self.cash.to_f64()
}
pub(crate) fn cash_fixed(&self) -> FixedMoney { self.cash }
pub(crate) fn financial_replay_identity(&self) -> serde_json::Value {
let positions = self.positions.iter().filter(|(_, position)| position.quantity > 0)
.map(|(symbol, position)| {
let mut lots = position.lots.iter().map(|lot| (lot.acquired_date, lot.quantity,
lot.entry_value.to_decimal_string(), lot.cost_basis.to_decimal_string())).collect::<Vec<_>>();
lots.sort();
(symbol.clone(), serde_json::json!({"quantity":position.quantity,"lots":lots,
"openedDate":position.opened_date,"lastBuyDate":position.last_buy_date}))
}).collect::<std::collections::BTreeMap<_, _>>();
let mut receivables = self.cash_receivables.iter().map(|row| (row.symbol.clone(), row.ex_date,
row.payable_date, fixed_money_or_panic(row.amount, "receivable identity").to_decimal_string(), row.reason.clone())).collect::<Vec<_>>();
receivables.sort();
serde_json::json!({"cash":self.cash.to_decimal_string(),"positions":positions,"receivables":receivables})
}
pub(crate) fn financial_position_basis(&self, symbol: &str) -> FixedMoney {
self.positions.get(symbol).map_or(FixedMoney::ZERO, Position::total_cost_basis)
}
pub(crate) fn replace_replayed_financial_book(&mut self, mut replayed: PortfolioState) -> Result<(), String> {
if replayed.cash < FixedMoney::ZERO || replayed.initial_cash != self.initial_cash {
return Err("manual corporate replay changed initial capital or borrowed cash".into());
}
for (symbol, position) in &mut replayed.positions {
if let Some(current) = self.positions.get(symbol).filter(|current| current.quantity > 0) {
position.last_price = current.last_price;
position.refresh_day_pnl();
}
}
self.cash = replayed.cash;
self.positions = replayed.positions;
self.cash_receivables = replayed.cash_receivables;
self.day_sold_symbols = replayed.day_sold_symbols;
// Existing issued units, explicit cash-flow/financing facts, and task
// target state are observed controls, not counterfactual new orders.
Ok(())
}
pub fn external_cash_flow_total(&self) -> f64 {
self.external_cash_flow_total.to_f64()
}
@@ -822,9 +868,13 @@ impl PortfolioState {
}
pub fn apply_cash_delta(&mut self, delta: f64) -> Result<(), String> {
self.apply_cash_delta_fixed(fixed_money(delta, "cash delta")?)
}
pub(crate) fn apply_cash_delta_fixed(&mut self, delta: FixedMoney) -> Result<(), String> {
self.cash = self
.cash
.checked_add(fixed_money(delta, "cash delta")?)
.checked_add(delta)
.ok_or_else(|| "fixed-point cash overflow".to_string())?;
Ok(())
}