使用真实标记日期区分交付计价并更新同日交付回归

This commit is contained in:
boris
2026-10-05 04:31:50 +08:00
parent 0caac44d52
commit 4eb4c17724
4 changed files with 29 additions and 5 deletions
+1 -1
View File
@@ -3244,7 +3244,7 @@ where
self.observe_manual_until(&mut manual_cursor, execution_date.and_hms_opt(0, 0, 0).unwrap(),
&mut portfolio, &mut result, &mut between_session_events, None)?;
self.retain_process_events(&mut result.process_events, &mut between_session_events);
portfolio.begin_trading_day();
portfolio.begin_trading_day_at(execution_date);
if let Some(account) = self.futures_account.as_mut() {
account.begin_trading_day();
}
@@ -360,7 +360,7 @@ impl ManualCorporateReplay {
return Err("manual corporate replay contains a future financial fact".into());
}
match event {
Event::Session => book.begin_trading_day(),
Event::Session => book.begin_trading_day_at(clock.date()),
Event::Cash(effect) => {
book.apply_cash_delta_fixed(effect.amount)?;
if book.cash_fixed() < FixedMoney::ZERO {
+26 -2
View File
@@ -96,6 +96,8 @@ pub struct Position {
// ALV-compatible moving average including buy costs; partial sells do not rebase it.
pub average_cost: f64,
pub last_price: f64,
last_market_mark_date: Option<NaiveDate>,
current_session_date: Option<NaiveDate>,
realized_pnl: FixedMoney,
realized_entry_pnl: FixedMoney,
pub trading_pnl: f64,
@@ -128,6 +130,8 @@ impl Position {
average_price: 0.0,
average_cost: 0.0,
last_price: 0.0,
last_market_mark_date: None,
current_session_date: None,
realized_pnl: FixedMoney::ZERO,
realized_entry_pnl: FixedMoney::ZERO,
trading_pnl: 0.0,
@@ -250,6 +254,7 @@ impl Position {
self.next_lot_sequence = self.next_lot_sequence.checked_add(1).expect("position lot sequence overflow");
self.quantity += quantity;
self.last_price = normalized_mark_price(mark_price, execution_price);
self.last_market_mark_date = Some(date);
self.day_trade_quantity_delta += quantity as i32;
self.day_buy_quantity += quantity;
self.day_buy_value = self
@@ -383,6 +388,7 @@ impl Position {
self.quantity -= quantity;
self.last_price = normalized_mark_price(mark_price, execution_price);
self.last_market_mark_date = self.current_session_date;
self.realized_pnl = self
.realized_pnl
.checked_add(realized)
@@ -806,6 +812,7 @@ pub struct PortfolioState {
closing_holdings: Arc<BTreeMap<NaiveDate, ClosingHoldings>>,
empty_simulation_origin: Option<NaiveDate>,
retired_lot_sequences: BTreeMap<String, u64>,
current_session_date: Option<NaiveDate>,
}
pub(crate) struct PortfolioCheckpoint {
@@ -930,8 +937,11 @@ impl PortfolioState {
if position.last_price > 0.0 && position.last_price.is_finite() {
// A purchase already marked on this ex-date has an actual ex-date
// quote. Earlier marks are translated to the accounting reference.
if !position.last_buy_date.is_some_and(|day| day >= action.date) && position.sold_quantity() == 0 {
if position.last_market_mark_date.is_some_and(|day| day < action.date) {
position.last_price /= ratio;
} else if position.last_market_mark_date.is_none() {
position.last_price = accounting_reference.filter(|price| price.is_finite() && *price > 0.0)
.ok_or("corporate_action_share_accounting_reference_missing")?;
}
} else {
position.last_price = accounting_reference.filter(|price| price.is_finite() && *price > 0.0)
@@ -1045,6 +1055,7 @@ impl PortfolioState {
closing_holdings: self.closing_holdings.clone(),
empty_simulation_origin: self.empty_simulation_origin,
retired_lot_sequences: self.retired_lot_sequences.clone(),
current_session_date: self.current_session_date,
},
position_order: Some(self.positions.keys().cloned().collect()),
}
@@ -1074,6 +1085,7 @@ impl PortfolioState {
closing_holdings: Arc::new(BTreeMap::new()),
empty_simulation_origin: None,
retired_lot_sequences: BTreeMap::new(),
current_session_date: None,
}
}
@@ -1178,6 +1190,8 @@ impl PortfolioState {
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.last_market_mark_date = current.last_market_mark_date;
position.current_session_date = current.current_session_date;
position.refresh_day_pnl();
}
}
@@ -1226,9 +1240,10 @@ impl PortfolioState {
pub fn position_mut(&mut self, symbol: &str) -> &mut Position {
let next = self.retired_lot_sequences.get(symbol).copied().unwrap_or(0);
let session = self.current_session_date;
self.positions
.entry(symbol.to_string())
.or_insert_with(|| { let mut position = Position::new(symbol); position.next_lot_sequence = next; position })
.or_insert_with(|| { let mut position = Position::new(symbol); position.next_lot_sequence = next; position.current_session_date = session; position })
}
pub fn apply_cash_delta(&mut self, delta: f64) -> Result<(), String> {
@@ -1339,6 +1354,8 @@ impl PortfolioState {
}
position.day_trade_cost = next_cost;
position.refresh_day_pnl();
position.current_session_date = Some(trade_date);
position.last_market_mark_date = Some(trade_date);
self.positions.insert(symbol.to_string(), position);
self.cash = next_cash;
Ok(delta)
@@ -1625,6 +1642,12 @@ impl PortfolioState {
self.refresh_dividend_receivables();
}
pub fn begin_trading_day_at(&mut self, date: NaiveDate) {
self.begin_trading_day();
self.current_session_date = Some(date);
for position in self.positions.values_mut() { position.current_session_date = Some(date); }
}
pub fn update_prices(
&mut self,
date: NaiveDate,
@@ -1686,6 +1709,7 @@ impl PortfolioState {
symbol: position.symbol.clone(),
})?;
position.last_price = price;
position.last_market_mark_date = Some(date);
position.refresh_day_pnl();
}
Ok(())
+1 -1
View File
@@ -664,7 +664,7 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
result
.position_events
.iter()
.any(|event| event.reason == "stock_split 1.200000" && event.quantity_after == 1_200)
.any(|event| event.reason == "registered_share_distribution_delivered" && event.quantity_after == 1_200)
);
assert!(
result