保留模拟器失败调用前的委托与执行状态

This commit is contained in:
boris
2026-09-15 01:28:38 +08:00
parent 4c6147e2ee
commit 695fdee4b8
6 changed files with 776 additions and 30 deletions
+73
View File
@@ -715,6 +715,27 @@ pub struct PortfolioState {
stock_pool_states: std::collections::BTreeMap<String,crate::stock_pool_state::StockPoolExecutionState>,
}
pub(crate) struct PortfolioCheckpoint {
saved: PortfolioState,
position_order: Option<Vec<String>>,
}
impl PortfolioCheckpoint {
pub(crate) fn restore(mut self, current: &mut PortfolioState) {
if let Some(order) = self.position_order.take() {
let mut positions = IndexMap::with_capacity(order.len());
for symbol in order {
let position = self.saved.positions.shift_remove(&symbol)
.or_else(|| current.positions.shift_remove(&symbol))
.expect("unchanged checkpoint position must remain present");
positions.insert(symbol, position);
}
self.saved.positions = positions;
}
*current = self.saved;
}
}
#[derive(Debug, Clone)]
pub struct PendingCashFlow {
pub payable_date: NaiveDate,
@@ -734,6 +755,29 @@ pub(crate) struct SuccessorConversionOutcome {
}
impl PortfolioState {
/// Ordinary single-security orders need not duplicate every other lot.
/// Complex portfolio intents request the complete checkpoint instead.
pub(crate) fn checkpoint(&self, symbols: Option<&BTreeSet<String>>) -> PortfolioCheckpoint {
let Some(symbols) = symbols else {
return PortfolioCheckpoint { saved: self.clone(), position_order: None };
};
PortfolioCheckpoint {
saved: Self {
initial_cash: self.initial_cash, units: self.units, cash: self.cash,
external_cash_flow_total: self.external_cash_flow_total,
cash_liabilities: self.cash_liabilities, management_fee_rate: self.management_fee_rate,
management_fees: self.management_fees,
// prune_flat_positions can remove an unrelated zero row.
positions: self.positions.iter().filter(|(symbol, position)| position.quantity == 0 || symbols.contains(*symbol))
.map(|(symbol, position)| (symbol.clone(), position.clone())).collect(),
cash_receivables: self.cash_receivables.clone(), pending_cash_flows: self.pending_cash_flows.clone(),
day_sold_symbols: self.day_sold_symbols.clone(), corporate_predecessors: self.corporate_predecessors.clone(),
stock_pool_states: self.stock_pool_states.clone(),
},
position_order: Some(self.positions.keys().cloned().collect()),
}
}
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");
@@ -1647,6 +1691,35 @@ mod tests {
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
PriceField,
};
#[test]
fn scoped_checkpoint_restores_order_flat_rows_and_progress_without_copying_untouched_lots() {
let date = NaiveDate::from_ymd_opt(2026,9,15).unwrap();
let mut portfolio = PortfolioState::new(10000.);
portfolio.position_mut("000001.SZ").buy(date,100,10.);
portfolio.position_mut("000002.SZ").buy(date,200,10.);
portfolio.position_mut("000003.SZ").buy(date,100,10.);
portfolio.position_mut("000003.SZ").sell(100,11.).unwrap();
let flat_realized = portfolio.position("000003.SZ").unwrap().realized_pnl;
let untouched_lots = portfolio.position("000002.SZ").unwrap().lots.as_ptr();
let before = portfolio.financial_replay_identity();
let order = portfolio.positions.keys().cloned().collect::<Vec<_>>();
let checkpoint = portfolio.checkpoint(Some(&BTreeSet::from(["000001.SZ".into(), "000004.SZ".into()])));
assert!(!checkpoint.saved.positions.contains_key("000002.SZ"));
portfolio.position_mut("000001.SZ").sell(100,11.).unwrap();
portfolio.prune_flat_positions();
portfolio.position_mut("000004.SZ").buy(date,100,12.);
portfolio.apply_cash_delta(100.).unwrap();
portfolio.stock_pool_states.insert("changed".into(), Default::default());
checkpoint.restore(&mut portfolio);
assert_eq!(portfolio.financial_replay_identity(), before);
assert_eq!(portfolio.positions.keys().cloned().collect::<Vec<_>>(), order);
assert_eq!(portfolio.position("000002.SZ").unwrap().lots.as_ptr(), untouched_lots);
assert_eq!(portfolio.position("000003.SZ").unwrap().realized_pnl, flat_realized);
assert!(portfolio.stock_pool_states.is_empty());
assert!(portfolio.position("000004.SZ").is_none());
}
#[test]
fn cash_ledger_accumulates_micro_yuan_exactly() {
let mut portfolio = PortfolioState::new(1_000_000.0);