修正持仓盈亏展示口径

This commit is contained in:
boris
2026-06-17 21:03:45 +08:00
parent 6c7f7130cf
commit 8d24badcf2
2 changed files with 85 additions and 49 deletions
+4 -18
View File
@@ -6190,17 +6190,10 @@ impl PlatformExprStrategy {
{ {
return Ok((false, false)); return Ok((false, false));
} }
let avg_price = if self.config.aiquant_transaction_cost let avg_price = position
&& position.average_cost.is_finite()
&& position.average_cost > 0.0
{
position.average_cost
} else {
position
.average_entry_price() .average_entry_price()
.filter(|value| value.is_finite() && *value > 0.0) .filter(|value| value.is_finite() && *value > 0.0)
.unwrap_or(position.average_cost) .unwrap_or(position.average_cost);
};
if position.quantity == 0 || avg_price <= 0.0 { if position.quantity == 0 || avg_price <= 0.0 {
return Ok((false, false)); return Ok((false, false));
} }
@@ -6696,17 +6689,10 @@ impl Strategy for PlatformExprStrategy {
continue; continue;
} }
let projected_position = projected.position(&position.symbol).unwrap_or(position); let projected_position = projected.position(&position.symbol).unwrap_or(position);
let avg_price = if self.config.aiquant_transaction_cost let avg_price = projected_position
&& projected_position.average_cost.is_finite()
&& projected_position.average_cost > 0.0
{
projected_position.average_cost
} else {
projected_position
.average_entry_price() .average_entry_price()
.filter(|value| value.is_finite() && *value > 0.0) .filter(|value| value.is_finite() && *value > 0.0)
.unwrap_or(projected_position.average_cost) .unwrap_or(projected_position.average_cost);
};
if projected_position.quantity == 0 || avg_price <= 0.0 { if projected_position.quantity == 0 || avg_price <= 0.0 {
continue; continue;
} }
+57 -7
View File
@@ -20,6 +20,7 @@ pub struct Position {
pub average_cost: f64, pub average_cost: f64,
pub last_price: f64, pub last_price: f64,
pub realized_pnl: f64, pub realized_pnl: f64,
realized_entry_pnl: f64,
pub trading_pnl: f64, pub trading_pnl: f64,
pub position_pnl: f64, pub position_pnl: f64,
pub dividend_receivable: f64, pub dividend_receivable: f64,
@@ -44,6 +45,7 @@ impl Position {
average_cost: 0.0, average_cost: 0.0,
last_price: 0.0, last_price: 0.0,
realized_pnl: 0.0, realized_pnl: 0.0,
realized_entry_pnl: 0.0,
trading_pnl: 0.0, trading_pnl: 0.0,
position_pnl: 0.0, position_pnl: 0.0,
dividend_receivable: 0.0, dividend_receivable: 0.0,
@@ -114,6 +116,7 @@ impl Position {
let mut remaining = quantity; let mut remaining = quantity;
let mut realized = 0.0; let mut realized = 0.0;
let mut realized_entry = 0.0;
while remaining > 0 { while remaining > 0 {
let Some(first_lot) = self.lots.first_mut() else { let Some(first_lot) = self.lots.first_mut() else {
@@ -122,6 +125,7 @@ impl Position {
let lot_sell = remaining.min(first_lot.quantity); let lot_sell = remaining.min(first_lot.quantity);
realized += (execution_price - first_lot.price) * lot_sell as f64; realized += (execution_price - first_lot.price) * lot_sell as f64;
realized_entry += (execution_price - first_lot.entry_price) * lot_sell as f64;
first_lot.quantity -= lot_sell; first_lot.quantity -= lot_sell;
remaining -= lot_sell; remaining -= lot_sell;
@@ -133,6 +137,7 @@ impl Position {
self.quantity -= quantity; self.quantity -= quantity;
self.last_price = normalized_mark_price(mark_price, execution_price); self.last_price = normalized_mark_price(mark_price, execution_price);
self.realized_pnl += realized; self.realized_pnl += realized;
self.realized_entry_pnl += realized_entry;
self.day_trade_quantity_delta -= quantity as i32; self.day_trade_quantity_delta -= quantity as i32;
self.day_sell_quantity += quantity; self.day_sell_quantity += quantity;
self.day_sell_value += execution_price * quantity as f64; self.day_sell_value += execution_price * quantity as f64;
@@ -157,10 +162,21 @@ impl Position {
(self.last_price - self.average_cost) * self.quantity as f64 (self.last_price - self.average_cost) * self.quantity as f64
} }
pub fn unrealized_entry_pnl(&self) -> f64 {
let Some(avg_price) = self.average_entry_price() else {
return 0.0;
};
(self.last_price - avg_price) * self.quantity as f64
}
pub fn pnl(&self) -> f64 { pub fn pnl(&self) -> f64 {
self.realized_pnl + self.unrealized_pnl() self.realized_pnl + self.unrealized_pnl()
} }
pub fn entry_pnl(&self) -> f64 {
self.realized_entry_pnl + self.unrealized_entry_pnl()
}
pub fn day_start_quantity(&self) -> u32 { pub fn day_start_quantity(&self) -> u32 {
self.day_start_quantity self.day_start_quantity
} }
@@ -765,21 +781,27 @@ impl PortfolioState {
self.positions self.positions
.values() .values()
.filter(|position| position.quantity > 0) .filter(|position| position.quantity > 0)
.map(|position| HoldingSummary { .map(|position| {
let market_value = position.market_value();
let entry_average_cost = position
.average_entry_price()
.filter(|value| value.is_finite() && *value > 0.0)
.unwrap_or(position.average_cost);
HoldingSummary {
date, date,
symbol: position.symbol.clone(), symbol: position.symbol.clone(),
quantity: position.quantity, quantity: position.quantity,
average_cost: position.average_cost, average_cost: entry_average_cost,
last_price: position.last_price, last_price: position.last_price,
market_value: position.market_value(), market_value,
value_percent: if total_equity > 0.0 { value_percent: if total_equity > 0.0 {
position.market_value() / total_equity market_value / total_equity
} else { } else {
0.0 0.0
}, },
unrealized_pnl: position.unrealized_pnl(), unrealized_pnl: position.unrealized_entry_pnl(),
realized_pnl: position.realized_pnl, realized_pnl: position.realized_entry_pnl,
pnl: position.pnl(), pnl: position.entry_pnl(),
trading_pnl: position.trading_pnl, trading_pnl: position.trading_pnl,
position_pnl: position.position_pnl, position_pnl: position.position_pnl,
dividend_receivable: position.dividend_receivable, dividend_receivable: position.dividend_receivable,
@@ -792,6 +814,7 @@ impl PortfolioState {
sold_value: position.sold_value(), sold_value: position.sold_value(),
transaction_cost: position.transaction_cost(), transaction_cost: position.transaction_cost(),
day_trade_quantity_delta: position.day_trade_quantity_delta(), day_trade_quantity_delta: position.day_trade_quantity_delta(),
}
}) })
.collect() .collect()
} }
@@ -815,6 +838,7 @@ impl PortfolioState {
let old_quantity = old_position.quantity; let old_quantity = old_position.quantity;
let last_price = old_position.last_price; let last_price = old_position.last_price;
let realized_pnl = old_position.realized_pnl; let realized_pnl = old_position.realized_pnl;
let realized_entry_pnl = old_position.realized_entry_pnl;
let mut converted_lots = old_position let mut converted_lots = old_position
.lots .lots
.into_iter() .into_iter()
@@ -851,6 +875,7 @@ impl PortfolioState {
successor.lots.extend(converted_lots); successor.lots.extend(converted_lots);
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum(); successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
successor.realized_pnl += realized_pnl; successor.realized_pnl += realized_pnl;
successor.realized_entry_pnl += realized_entry_pnl;
if converted_last_price > 0.0 { if converted_last_price > 0.0 {
successor.last_price = converted_last_price; successor.last_price = converted_last_price;
} }
@@ -930,6 +955,31 @@ mod tests {
assert!((position.holding_return(6.06).unwrap() - (6.06 / 5.66 - 1.0)).abs() < 1e-12); assert!((position.holding_return(6.06).unwrap() - (6.06 / 5.66 - 1.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();
let mut portfolio = PortfolioState::new(10_000.0);
{
let position = portfolio.position_mut("600561.SH");
position.buy(date, 100, 10.0);
position.record_buy_trade_cost(100, 5.0);
position.last_price = 10.5;
}
let summary = portfolio.holdings_summary(date);
assert_eq!(summary.len(), 1);
assert!((summary[0].average_cost - 10.0).abs() < 1e-12);
assert!((summary[0].unrealized_pnl - 50.0).abs() < 1e-12);
assert!((summary[0].realized_pnl - 0.0).abs() < 1e-12);
assert!(
portfolio
.position("600561.SH")
.expect("position")
.average_cost
> summary[0].average_cost
);
}
#[test] #[test]
fn cash_dividend_can_preserve_avg_cost_for_aiquant_compatibility() { fn cash_dividend_can_preserve_avg_cost_for_aiquant_compatibility() {
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(); let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();