补充持仓移动平均成交价
This commit is contained in:
@@ -60,6 +60,8 @@ pub struct PositionLot {
|
||||
pub struct Position {
|
||||
pub symbol: String,
|
||||
pub quantity: u32,
|
||||
// ALV-compatible moving average execution price; partial sells do not rebase it.
|
||||
pub average_price: f64,
|
||||
// ALV-compatible moving average including buy costs; partial sells do not rebase it.
|
||||
pub average_cost: f64,
|
||||
pub last_price: f64,
|
||||
@@ -86,6 +88,7 @@ impl Position {
|
||||
Self {
|
||||
symbol: symbol.into(),
|
||||
quantity: 0,
|
||||
average_price: 0.0,
|
||||
average_cost: 0.0,
|
||||
last_price: 0.0,
|
||||
realized_pnl: FixedMoney::ZERO,
|
||||
@@ -127,6 +130,7 @@ impl Position {
|
||||
}
|
||||
|
||||
let previous_quantity = self.quantity;
|
||||
let previous_average_price = self.average_price;
|
||||
let previous_average_cost = self.average_cost;
|
||||
let gross_amount = fixed_money_or_panic(
|
||||
execution_price * quantity as f64,
|
||||
@@ -146,6 +150,18 @@ impl Position {
|
||||
.day_buy_value
|
||||
.checked_add(gross_amount)
|
||||
.expect("fixed-point day buy value overflow");
|
||||
if previous_quantity > 0
|
||||
&& previous_average_price.is_finite()
|
||||
&& previous_average_price > 0.0
|
||||
&& execution_price.is_finite()
|
||||
&& execution_price > 0.0
|
||||
{
|
||||
self.average_price = (previous_average_price * previous_quantity as f64
|
||||
+ execution_price * quantity as f64)
|
||||
/ self.quantity as f64;
|
||||
} else {
|
||||
self.average_price = execution_price;
|
||||
}
|
||||
if previous_quantity > 0
|
||||
&& previous_average_cost.is_finite()
|
||||
&& previous_average_cost > 0.0
|
||||
@@ -186,6 +202,7 @@ impl Position {
|
||||
let mut remaining_proceeds = total_proceeds;
|
||||
let mut realized = FixedMoney::ZERO;
|
||||
let mut realized_entry = FixedMoney::ZERO;
|
||||
let average_price_before_sell = self.average_price;
|
||||
let average_cost_before_sell = self.average_cost;
|
||||
|
||||
while remaining > 0 {
|
||||
@@ -250,11 +267,19 @@ impl Position {
|
||||
.checked_add(total_proceeds)
|
||||
.ok_or_else(|| "fixed-point day sell value overflow".to_string())?;
|
||||
if self.quantity == 0 {
|
||||
self.average_price = 0.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();
|
||||
if average_price_before_sell.is_finite() && average_price_before_sell > 0.0 {
|
||||
self.average_price = average_price_before_sell;
|
||||
} else {
|
||||
self.average_price = self.average_entry_price().unwrap_or(0.0);
|
||||
}
|
||||
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.to_f64())
|
||||
@@ -421,9 +446,12 @@ impl Position {
|
||||
}
|
||||
|
||||
pub fn holding_return(&self, price: f64) -> Option<f64> {
|
||||
let Some(avg_price) = self.average_entry_price() else {
|
||||
return None;
|
||||
};
|
||||
let avg_price = self
|
||||
.average_price
|
||||
.is_finite()
|
||||
.then_some(self.average_price)
|
||||
.filter(|value| *value > 0.0)
|
||||
.or_else(|| self.average_entry_price())?;
|
||||
if avg_price <= 0.0 {
|
||||
None
|
||||
} else {
|
||||
@@ -503,6 +531,7 @@ impl Position {
|
||||
if adjust_cost_basis {
|
||||
self.average_cost -= dividend_per_share;
|
||||
}
|
||||
self.average_price -= dividend_per_share;
|
||||
self.last_price -= dividend_per_share;
|
||||
self.day_dividend_cash = self
|
||||
.day_dividend_cash
|
||||
@@ -545,6 +574,11 @@ impl Position {
|
||||
self.lots = scaled_lots;
|
||||
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
|
||||
self.last_price /= ratio;
|
||||
if self.average_price.is_finite() && self.average_price > 0.0 {
|
||||
self.average_price /= ratio;
|
||||
} else {
|
||||
self.average_price = self.average_entry_price().unwrap_or(0.0);
|
||||
}
|
||||
if self.average_cost.is_finite() && self.average_cost > 0.0 {
|
||||
self.average_cost /= ratio;
|
||||
} else {
|
||||
@@ -1128,8 +1162,11 @@ impl PortfolioState {
|
||||
.map(|position| {
|
||||
let market_value = position.market_value();
|
||||
let entry_average_cost = position
|
||||
.average_entry_price()
|
||||
.average_price
|
||||
.is_finite()
|
||||
.then_some(position.average_price)
|
||||
.filter(|value| value.is_finite() && *value > 0.0)
|
||||
.or_else(|| position.average_entry_price())
|
||||
.unwrap_or(position.average_cost);
|
||||
HoldingSummary {
|
||||
date,
|
||||
@@ -1181,6 +1218,7 @@ impl PortfolioState {
|
||||
|
||||
let old_quantity = old_position.quantity;
|
||||
let last_price = old_position.last_price;
|
||||
let old_average_price = old_position.average_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;
|
||||
@@ -1218,6 +1256,7 @@ impl PortfolioState {
|
||||
.entry(new_symbol.to_string())
|
||||
.or_insert_with(|| Position::new(new_symbol));
|
||||
let successor_quantity_before = successor.quantity;
|
||||
let successor_average_price_before = successor.average_price;
|
||||
let successor_average_cost_before = successor.average_cost;
|
||||
successor.lots.extend(converted_lots);
|
||||
successor.quantity = successor.lots.iter().map(|lot| lot.quantity).sum();
|
||||
@@ -1232,6 +1271,30 @@ impl PortfolioState {
|
||||
if converted_last_price > 0.0 {
|
||||
successor.last_price = converted_last_price;
|
||||
}
|
||||
let converted_average_price = if old_average_price.is_finite()
|
||||
&& old_average_price > 0.0
|
||||
&& ratio.is_finite()
|
||||
&& ratio > 0.0
|
||||
{
|
||||
Some(old_average_price / ratio)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(converted_average_price) = converted_average_price {
|
||||
if successor_quantity_before > 0
|
||||
&& successor_average_price_before.is_finite()
|
||||
&& successor_average_price_before > 0.0
|
||||
{
|
||||
successor.average_price = (successor_average_price_before
|
||||
* successor_quantity_before as f64
|
||||
+ converted_average_price * converted_quantity as f64)
|
||||
/ successor.quantity as f64;
|
||||
} else {
|
||||
successor.average_price = converted_average_price;
|
||||
}
|
||||
} else {
|
||||
successor.average_price = successor.average_entry_price().unwrap_or(0.0);
|
||||
}
|
||||
let converted_average_cost = if old_average_cost.is_finite()
|
||||
&& old_average_cost > 0.0
|
||||
&& ratio.is_finite()
|
||||
@@ -1375,6 +1438,7 @@ mod tests {
|
||||
let realized = position.sell(100, 6.0).expect("partial FIFO sell");
|
||||
|
||||
assert_eq!(position.quantity, 100);
|
||||
assert!((position.average_price - 7.5).abs() < 1e-12);
|
||||
assert!((position.average_cost - 7.55).abs() < 1e-12);
|
||||
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
||||
assert!((realized + 405.0).abs() < 1e-12);
|
||||
@@ -1409,6 +1473,7 @@ mod tests {
|
||||
position.record_buy_trade_cost(22_200, 100.0);
|
||||
|
||||
assert!(position.average_cost > 5.66);
|
||||
assert!((position.average_price - 5.66).abs() < 1e-12);
|
||||
assert!((position.average_entry_price().unwrap() - 5.66).abs() < 1e-12);
|
||||
assert!((position.holding_return(6.06).unwrap() - (6.06 / 5.66 - 1.0)).abs() < 1e-12);
|
||||
}
|
||||
@@ -1430,6 +1495,7 @@ mod tests {
|
||||
position.sell(2700, 16.8331).expect("partial sell");
|
||||
|
||||
assert_eq!(position.quantity, 100);
|
||||
assert!((position.average_price - 18.94711428571429).abs() < 1e-12);
|
||||
assert!((position.average_cost - average_cost_before).abs() < 1e-12);
|
||||
}
|
||||
|
||||
@@ -1443,11 +1509,13 @@ mod tests {
|
||||
|
||||
position.sell(100, 6.0).expect("partial sell");
|
||||
assert_eq!(position.quantity, 100);
|
||||
assert!((position.average_price - 7.5).abs() < 1e-12);
|
||||
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_price - 6.25).abs() < 1e-12);
|
||||
assert!((position.average_cost - 6.25).abs() < 1e-12);
|
||||
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
||||
}
|
||||
@@ -1489,6 +1557,7 @@ mod tests {
|
||||
let cash = position.apply_cash_dividend_preserve_cost_basis(0.6);
|
||||
|
||||
assert!((cash - 600.0).abs() < 1e-12);
|
||||
assert!((position.average_price - 45.85).abs() < 1e-12);
|
||||
assert!((position.average_cost - cost_before).abs() < 1e-12);
|
||||
assert!((position.average_entry_price().unwrap() - (entry_before - 0.6)).abs() < 1e-12);
|
||||
assert!((position.last_price - 45.85).abs() < 1e-12);
|
||||
|
||||
Reference in New Issue
Block a user