From 4eb4c177243cabc88659291bc24680bbb3d4fdb6 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 5 Oct 2026 04:31:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9C=9F=E5=AE=9E=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E6=97=A5=E6=9C=9F=E5=8C=BA=E5=88=86=E4=BA=A4=E4=BB=98?= =?UTF-8?q?=E8=AE=A1=E4=BB=B7=E5=B9=B6=E6=9B=B4=E6=96=B0=E5=90=8C=E6=97=A5?= =?UTF-8?q?=E4=BA=A4=E4=BB=98=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/engine.rs | 2 +- .../fidc-core/src/manual_corporate_replay.rs | 2 +- crates/fidc-core/src/portfolio.rs | 28 +++++++++++++++++-- crates/fidc-core/tests/corporate_actions.rs | 2 +- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 24908ad..fd730aa 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -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(); } diff --git a/crates/fidc-core/src/manual_corporate_replay.rs b/crates/fidc-core/src/manual_corporate_replay.rs index 1a24961..2c8648e 100644 --- a/crates/fidc-core/src/manual_corporate_replay.rs +++ b/crates/fidc-core/src/manual_corporate_replay.rs @@ -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 { diff --git a/crates/fidc-core/src/portfolio.rs b/crates/fidc-core/src/portfolio.rs index e76f419..ba96e47 100644 --- a/crates/fidc-core/src/portfolio.rs +++ b/crates/fidc-core/src/portfolio.rs @@ -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, + current_session_date: Option, 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>, empty_simulation_origin: Option, retired_lot_sequences: BTreeMap, + current_session_date: Option, } 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(()) diff --git a/crates/fidc-core/tests/corporate_actions.rs b/crates/fidc-core/tests/corporate_actions.rs index 41bc306..86dfb27 100644 --- a/crates/fidc-core/tests/corporate_actions.rs +++ b/crates/fidc-core/tests/corporate_actions.rs @@ -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