From 4ce52a7af6803e209201f827d7a6d775bbab700a Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 8 Jul 2026 02:44:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=83=A8=E5=88=86=E5=8D=96?= =?UTF-8?q?=E5=87=BA=E6=8C=81=E4=BB=93=E6=88=90=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/portfolio.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/portfolio.rs b/crates/fidc-core/src/portfolio.rs index 17a40b3..07294ab 100644 --- a/crates/fidc-core/src/portfolio.rs +++ b/crates/fidc-core/src/portfolio.rs @@ -117,6 +117,7 @@ impl Position { let mut remaining = quantity; let mut realized = 0.0; let mut realized_entry = 0.0; + let average_cost_before_sell = self.average_cost; while remaining > 0 { let Some(first_lot) = self.lots.first_mut() else { @@ -141,7 +142,13 @@ impl Position { self.day_trade_quantity_delta -= quantity as i32; self.day_sell_quantity += quantity; self.day_sell_value += execution_price * quantity as f64; - self.recalculate_average_cost(); + if self.quantity == 0 { + self.recalculate_average_cost(); + } else if average_cost_before_sell.is_finite() && average_cost_before_sell > 0.0 { + self.average_cost = average_cost_before_sell; + } else { + self.recalculate_average_cost(); + } self.refresh_day_pnl(); Ok(realized) } @@ -955,6 +962,26 @@ mod tests { assert!((position.holding_return(6.06).unwrap() - (6.06 / 5.66 - 1.0)).abs() < 1e-12); } + #[test] + fn partial_sell_preserves_remaining_average_cost() { + let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(); + let mut position = Position::new("603958.SH"); + position.buy(date, 800, 18.0981); + position.record_buy_trade_cost(800, 5.0); + position.buy(date, 1700, 19.4694); + position.record_buy_trade_cost(1700, 8.27451625); + position.buy(date, 200, 18.4584); + position.record_buy_trade_cost(200, 5.0); + position.buy(date, 100, 17.8378); + position.record_buy_trade_cost(100, 5.0); + let average_cost_before = position.average_cost; + + position.sell(2700, 16.8331).expect("partial sell"); + + assert_eq!(position.quantity, 100); + assert!((position.average_cost - average_cost_before).abs() < 1e-12); + } + #[test] fn holdings_summary_reports_entry_price_pnl_excluding_buy_commission() { let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();