修正AiQuant兼容持仓成本止损口径

This commit is contained in:
boris
2026-07-10 10:35:58 +08:00
parent f7d0889bbc
commit e396c895dc
3 changed files with 255 additions and 9 deletions
+70 -4
View File
@@ -82,6 +82,8 @@ impl Position {
return;
}
let previous_quantity = self.quantity;
let previous_average_cost = self.average_cost;
self.lots.push(PositionLot {
acquired_date: date,
quantity,
@@ -93,7 +95,18 @@ impl Position {
self.day_trade_quantity_delta += quantity as i32;
self.day_buy_quantity += quantity;
self.day_buy_value += execution_price * quantity as f64;
self.recalculate_average_cost();
if previous_quantity > 0
&& previous_average_cost.is_finite()
&& previous_average_cost > 0.0
&& execution_price.is_finite()
&& execution_price > 0.0
{
self.average_cost = (previous_average_cost * previous_quantity as f64
+ execution_price * quantity as f64)
/ self.quantity as f64;
} else {
self.recalculate_average_cost();
}
self.refresh_day_pnl();
}
@@ -259,7 +272,11 @@ impl Position {
}
if let Some(lot) = self.lots.last_mut() {
lot.price += cost / quantity as f64;
self.recalculate_average_cost();
if self.quantity > 0 && self.average_cost.is_finite() && self.average_cost > 0.0 {
self.average_cost += cost / self.quantity as f64;
} else {
self.recalculate_average_cost();
}
}
self.day_trade_cost += cost;
self.refresh_day_pnl();
@@ -377,7 +394,11 @@ impl Position {
self.lots = scaled_lots;
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
self.last_price /= ratio;
self.recalculate_average_cost();
if self.average_cost.is_finite() && self.average_cost > 0.0 {
self.average_cost /= ratio;
} else {
self.recalculate_average_cost();
}
self.day_split_ratio *= ratio;
self.refresh_day_pnl();
self.quantity as i32 - old_quantity as i32
@@ -844,6 +865,7 @@ impl PortfolioState {
let old_quantity = old_position.quantity;
let last_price = old_position.last_price;
let old_average_cost = old_position.average_cost;
let realized_pnl = old_position.realized_pnl;
let realized_entry_pnl = old_position.realized_entry_pnl;
let mut converted_lots = old_position
@@ -879,6 +901,8 @@ impl PortfolioState {
.positions
.entry(new_symbol.to_string())
.or_insert_with(|| Position::new(new_symbol));
let successor_quantity_before = successor.quantity;
let successor_average_cost_before = successor.average_cost;
successor.lots.extend(converted_lots);
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
successor.realized_pnl += realized_pnl;
@@ -886,7 +910,30 @@ impl PortfolioState {
if converted_last_price > 0.0 {
successor.last_price = converted_last_price;
}
successor.recalculate_average_cost();
let converted_average_cost = if old_average_cost.is_finite()
&& old_average_cost > 0.0
&& ratio.is_finite()
&& ratio > 0.0
{
Some(old_average_cost / ratio)
} else {
None
};
if let Some(converted_average_cost) = converted_average_cost {
if successor_quantity_before > 0
&& successor_average_cost_before.is_finite()
&& successor_average_cost_before > 0.0
{
successor.average_cost = (successor_average_cost_before
* successor_quantity_before as f64
+ converted_average_cost * converted_quantity as f64)
/ successor.quantity as f64;
} else {
successor.average_cost = converted_average_cost;
}
} else {
successor.recalculate_average_cost();
}
successor.refresh_day_pnl();
Some(SuccessorConversionOutcome {
@@ -982,6 +1029,25 @@ mod tests {
assert!((position.average_cost - average_cost_before).abs() < 1e-12);
}
#[test]
fn buy_after_partial_sell_continues_moving_average_cost_basis() {
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let mut position = Position::new("300405.SZ");
position.buy(date, 100, 10.0);
position.buy(date, 100, 5.0);
assert!((position.average_cost - 7.5).abs() < 1e-12);
position.sell(100, 6.0).expect("partial sell");
assert_eq!(position.quantity, 100);
assert!((position.average_cost - 7.5).abs() < 1e-12);
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
position.buy(date, 100, 5.0);
assert_eq!(position.quantity, 200);
assert!((position.average_cost - 6.25).abs() < 1e-12);
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
}
#[test]
fn holdings_summary_reports_entry_price_pnl_excluding_buy_commission() {
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();