修正持仓盈亏展示口径
This commit is contained in:
@@ -6190,17 +6190,10 @@ impl PlatformExprStrategy {
|
||||
{
|
||||
return Ok((false, false));
|
||||
}
|
||||
let avg_price = if self.config.aiquant_transaction_cost
|
||||
&& position.average_cost.is_finite()
|
||||
&& position.average_cost > 0.0
|
||||
{
|
||||
position.average_cost
|
||||
} else {
|
||||
position
|
||||
let avg_price = position
|
||||
.average_entry_price()
|
||||
.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 {
|
||||
return Ok((false, false));
|
||||
}
|
||||
@@ -6696,17 +6689,10 @@ impl Strategy for PlatformExprStrategy {
|
||||
continue;
|
||||
}
|
||||
let projected_position = projected.position(&position.symbol).unwrap_or(position);
|
||||
let avg_price = if self.config.aiquant_transaction_cost
|
||||
&& projected_position.average_cost.is_finite()
|
||||
&& projected_position.average_cost > 0.0
|
||||
{
|
||||
projected_position.average_cost
|
||||
} else {
|
||||
projected_position
|
||||
let avg_price = projected_position
|
||||
.average_entry_price()
|
||||
.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 {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct Position {
|
||||
pub average_cost: f64,
|
||||
pub last_price: f64,
|
||||
pub realized_pnl: f64,
|
||||
realized_entry_pnl: f64,
|
||||
pub trading_pnl: f64,
|
||||
pub position_pnl: f64,
|
||||
pub dividend_receivable: f64,
|
||||
@@ -44,6 +45,7 @@ impl Position {
|
||||
average_cost: 0.0,
|
||||
last_price: 0.0,
|
||||
realized_pnl: 0.0,
|
||||
realized_entry_pnl: 0.0,
|
||||
trading_pnl: 0.0,
|
||||
position_pnl: 0.0,
|
||||
dividend_receivable: 0.0,
|
||||
@@ -114,6 +116,7 @@ impl Position {
|
||||
|
||||
let mut remaining = quantity;
|
||||
let mut realized = 0.0;
|
||||
let mut realized_entry = 0.0;
|
||||
|
||||
while remaining > 0 {
|
||||
let Some(first_lot) = self.lots.first_mut() else {
|
||||
@@ -122,6 +125,7 @@ impl Position {
|
||||
|
||||
let lot_sell = remaining.min(first_lot.quantity);
|
||||
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;
|
||||
remaining -= lot_sell;
|
||||
|
||||
@@ -133,6 +137,7 @@ impl Position {
|
||||
self.quantity -= quantity;
|
||||
self.last_price = normalized_mark_price(mark_price, execution_price);
|
||||
self.realized_pnl += realized;
|
||||
self.realized_entry_pnl += realized_entry;
|
||||
self.day_trade_quantity_delta -= quantity as i32;
|
||||
self.day_sell_quantity += quantity;
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
self.day_start_quantity
|
||||
}
|
||||
@@ -765,21 +781,27 @@ impl PortfolioState {
|
||||
self.positions
|
||||
.values()
|
||||
.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,
|
||||
symbol: position.symbol.clone(),
|
||||
quantity: position.quantity,
|
||||
average_cost: position.average_cost,
|
||||
average_cost: entry_average_cost,
|
||||
last_price: position.last_price,
|
||||
market_value: position.market_value(),
|
||||
market_value,
|
||||
value_percent: if total_equity > 0.0 {
|
||||
position.market_value() / total_equity
|
||||
market_value / total_equity
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
unrealized_pnl: position.unrealized_pnl(),
|
||||
realized_pnl: position.realized_pnl,
|
||||
pnl: position.pnl(),
|
||||
unrealized_pnl: position.unrealized_entry_pnl(),
|
||||
realized_pnl: position.realized_entry_pnl,
|
||||
pnl: position.entry_pnl(),
|
||||
trading_pnl: position.trading_pnl,
|
||||
position_pnl: position.position_pnl,
|
||||
dividend_receivable: position.dividend_receivable,
|
||||
@@ -792,6 +814,7 @@ impl PortfolioState {
|
||||
sold_value: position.sold_value(),
|
||||
transaction_cost: position.transaction_cost(),
|
||||
day_trade_quantity_delta: position.day_trade_quantity_delta(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -815,6 +838,7 @@ impl PortfolioState {
|
||||
let old_quantity = old_position.quantity;
|
||||
let last_price = old_position.last_price;
|
||||
let realized_pnl = old_position.realized_pnl;
|
||||
let realized_entry_pnl = old_position.realized_entry_pnl;
|
||||
let mut converted_lots = old_position
|
||||
.lots
|
||||
.into_iter()
|
||||
@@ -851,6 +875,7 @@ impl PortfolioState {
|
||||
successor.lots.extend(converted_lots);
|
||||
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
|
||||
successor.realized_pnl += realized_pnl;
|
||||
successor.realized_entry_pnl += realized_entry_pnl;
|
||||
if converted_last_price > 0.0 {
|
||||
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);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn cash_dividend_can_preserve_avg_cost_for_aiquant_compatibility() {
|
||||
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user