保留模拟器失败调用前的委托与执行状态
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user