让实际换股持仓继承原策略保护期限

This commit is contained in:
boris
2026-09-14 23:38:43 +08:00
parent ad76bdb6ae
commit ba4b77fd74
8 changed files with 370 additions and 44 deletions
+29 -3
View File
@@ -709,6 +709,9 @@ pub struct PortfolioState {
cash_receivables: Vec<CashReceivable>,
pending_cash_flows: Vec<PendingCashFlow>,
day_sold_symbols: BTreeSet<String>,
// Observed holding conversions, never a catalog alias or a new target.
// Kept after a position becomes flat so an active date lock is not lost.
corporate_predecessors: BTreeMap<String, BTreeSet<String>>,
stock_pool_states: std::collections::BTreeMap<String,crate::stock_pool_state::StockPoolExecutionState>,
}
@@ -750,6 +753,7 @@ impl PortfolioState {
cash_receivables: Vec::new(),
pending_cash_flows: Vec::new(),
day_sold_symbols: BTreeSet::new(),
corporate_predecessors: BTreeMap::new(),
stock_pool_states: std::collections::BTreeMap::new(),
}
}
@@ -762,6 +766,14 @@ impl PortfolioState {
pub(crate) fn initial_cash_fixed(&self) -> FixedMoney { self.initial_cash }
pub(crate) fn corporate_predecessors(&self, symbol: &str) -> impl Iterator<Item = &str> {
self.corporate_predecessors.get(symbol).into_iter().flatten().map(String::as_str)
}
pub(crate) fn observed_successor_symbols(&self) -> impl Iterator<Item = &str> {
self.corporate_predecessors.keys().map(String::as_str)
}
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()
}
@@ -807,7 +819,11 @@ impl PortfolioState {
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})
let mut identity = serde_json::json!({"cash":self.cash.to_decimal_string(),"positions":positions,"receivables":receivables});
if !self.corporate_predecessors.is_empty() {
identity["corporatePredecessors"] = serde_json::json!(self.corporate_predecessors);
}
identity
}
pub(crate) fn financial_position_basis(&self, symbol: &str) -> FixedMoney {
@@ -828,6 +844,9 @@ impl PortfolioState {
self.positions = replayed.positions;
self.cash_receivables = replayed.cash_receivables;
self.day_sold_symbols = replayed.day_sold_symbols;
// Corrected actual receipts can prove a position was fully sold
// before conversion. Do not retain a now-disproved financial link.
self.corporate_predecessors = replayed.corporate_predecessors;
// Existing issued units, explicit cash-flow/financing facts, and task
// target state are observed controls, not counterfactual new orders.
Ok(())
@@ -1557,7 +1576,7 @@ impl PortfolioState {
}
successor.refresh_day_pnl();
Some(SuccessorConversionOutcome {
let outcome = SuccessorConversionOutcome {
old_symbol: old_symbol_owned,
new_symbol: new_symbol.to_string(),
old_quantity,
@@ -1569,7 +1588,14 @@ impl PortfolioState {
} else {
0.0
},
})
};
if converted_quantity > 0 {
let mut predecessors = self.corporate_predecessors.get(old_symbol).cloned().unwrap_or_default();
predecessors.insert(old_symbol.to_owned());
predecessors.remove(new_symbol);
self.corporate_predecessors.entry(new_symbol.to_owned()).or_default().extend(predecessors);
}
Some(outcome)
}
fn sum_fixed_money(