补充持仓移动平均成交价
This commit is contained in:
@@ -11664,8 +11664,11 @@ impl PlatformExprStrategy {
|
|||||||
return Ok((false, false));
|
return Ok((false, false));
|
||||||
}
|
}
|
||||||
let entry_avg_price = position
|
let entry_avg_price = position
|
||||||
.average_entry_price()
|
.average_price
|
||||||
.filter(|value| value.is_finite() && *value > 0.0)
|
.is_finite()
|
||||||
|
.then_some(position.average_price)
|
||||||
|
.filter(|value| *value > 0.0)
|
||||||
|
.or_else(|| position.average_entry_price())
|
||||||
.unwrap_or(position.average_cost);
|
.unwrap_or(position.average_cost);
|
||||||
let stop_take_base_price = match self.config.stop_take_reference_price_mode {
|
let stop_take_base_price = match self.config.stop_take_reference_price_mode {
|
||||||
PlatformStopTakeReferencePriceMode::PositionCostBasis => {
|
PlatformStopTakeReferencePriceMode::PositionCostBasis => {
|
||||||
@@ -12997,8 +13000,11 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
let projected_position = projected.position(&position.symbol).unwrap_or(position);
|
let projected_position = projected.position(&position.symbol).unwrap_or(position);
|
||||||
let avg_price = projected_position
|
let avg_price = projected_position
|
||||||
.average_entry_price()
|
.average_price
|
||||||
.filter(|value| value.is_finite() && *value > 0.0)
|
.is_finite()
|
||||||
|
.then_some(projected_position.average_price)
|
||||||
|
.filter(|value| *value > 0.0)
|
||||||
|
.or_else(|| projected_position.average_entry_price())
|
||||||
.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;
|
||||||
@@ -34937,7 +34943,7 @@ mod tests {
|
|||||||
cfg.benchmark_short_ma_days = 1;
|
cfg.benchmark_short_ma_days = 1;
|
||||||
cfg.benchmark_long_ma_days = 1;
|
cfg.benchmark_long_ma_days = 1;
|
||||||
cfg.stop_loss_expr = concat!(
|
cfg.stop_loss_expr = concat!(
|
||||||
"order_book_id == \"000001.SZ\" && avg_price > avg_cost",
|
"order_book_id == \"000001.SZ\" && avg_price == 8.5 && avg_cost == 8.5",
|
||||||
" && sellable == sellable_qty && closable == sellable_qty",
|
" && sellable == sellable_qty && closable == sellable_qty",
|
||||||
" && equity == position_market_value",
|
" && equity == position_market_value",
|
||||||
" && position_prev_close == prev_position_close",
|
" && position_prev_close == prev_position_close",
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ pub struct PositionLot {
|
|||||||
pub struct Position {
|
pub struct Position {
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
pub quantity: u32,
|
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.
|
// ALV-compatible moving average including buy costs; partial sells do not rebase it.
|
||||||
pub average_cost: f64,
|
pub average_cost: f64,
|
||||||
pub last_price: f64,
|
pub last_price: f64,
|
||||||
@@ -86,6 +88,7 @@ impl Position {
|
|||||||
Self {
|
Self {
|
||||||
symbol: symbol.into(),
|
symbol: symbol.into(),
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
|
average_price: 0.0,
|
||||||
average_cost: 0.0,
|
average_cost: 0.0,
|
||||||
last_price: 0.0,
|
last_price: 0.0,
|
||||||
realized_pnl: FixedMoney::ZERO,
|
realized_pnl: FixedMoney::ZERO,
|
||||||
@@ -127,6 +130,7 @@ impl Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let previous_quantity = self.quantity;
|
let previous_quantity = self.quantity;
|
||||||
|
let previous_average_price = self.average_price;
|
||||||
let previous_average_cost = self.average_cost;
|
let previous_average_cost = self.average_cost;
|
||||||
let gross_amount = fixed_money_or_panic(
|
let gross_amount = fixed_money_or_panic(
|
||||||
execution_price * quantity as f64,
|
execution_price * quantity as f64,
|
||||||
@@ -146,6 +150,18 @@ impl Position {
|
|||||||
.day_buy_value
|
.day_buy_value
|
||||||
.checked_add(gross_amount)
|
.checked_add(gross_amount)
|
||||||
.expect("fixed-point day buy value overflow");
|
.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
|
if previous_quantity > 0
|
||||||
&& previous_average_cost.is_finite()
|
&& previous_average_cost.is_finite()
|
||||||
&& previous_average_cost > 0.0
|
&& previous_average_cost > 0.0
|
||||||
@@ -186,6 +202,7 @@ impl Position {
|
|||||||
let mut remaining_proceeds = total_proceeds;
|
let mut remaining_proceeds = total_proceeds;
|
||||||
let mut realized = FixedMoney::ZERO;
|
let mut realized = FixedMoney::ZERO;
|
||||||
let mut realized_entry = FixedMoney::ZERO;
|
let mut realized_entry = FixedMoney::ZERO;
|
||||||
|
let average_price_before_sell = self.average_price;
|
||||||
let average_cost_before_sell = self.average_cost;
|
let average_cost_before_sell = self.average_cost;
|
||||||
|
|
||||||
while remaining > 0 {
|
while remaining > 0 {
|
||||||
@@ -250,12 +267,20 @@ impl Position {
|
|||||||
.checked_add(total_proceeds)
|
.checked_add(total_proceeds)
|
||||||
.ok_or_else(|| "fixed-point day sell value overflow".to_string())?;
|
.ok_or_else(|| "fixed-point day sell value overflow".to_string())?;
|
||||||
if self.quantity == 0 {
|
if self.quantity == 0 {
|
||||||
|
self.average_price = 0.0;
|
||||||
self.recalculate_average_cost();
|
self.recalculate_average_cost();
|
||||||
} else if average_cost_before_sell.is_finite() && average_cost_before_sell > 0.0 {
|
} else {
|
||||||
|
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;
|
self.average_cost = average_cost_before_sell;
|
||||||
} else {
|
} else {
|
||||||
self.recalculate_average_cost();
|
self.recalculate_average_cost();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.refresh_day_pnl();
|
self.refresh_day_pnl();
|
||||||
Ok(realized.to_f64())
|
Ok(realized.to_f64())
|
||||||
}
|
}
|
||||||
@@ -421,9 +446,12 @@ impl Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn holding_return(&self, price: f64) -> Option<f64> {
|
pub fn holding_return(&self, price: f64) -> Option<f64> {
|
||||||
let Some(avg_price) = self.average_entry_price() else {
|
let avg_price = self
|
||||||
return None;
|
.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 {
|
if avg_price <= 0.0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -503,6 +531,7 @@ impl Position {
|
|||||||
if adjust_cost_basis {
|
if adjust_cost_basis {
|
||||||
self.average_cost -= dividend_per_share;
|
self.average_cost -= dividend_per_share;
|
||||||
}
|
}
|
||||||
|
self.average_price -= dividend_per_share;
|
||||||
self.last_price -= dividend_per_share;
|
self.last_price -= dividend_per_share;
|
||||||
self.day_dividend_cash = self
|
self.day_dividend_cash = self
|
||||||
.day_dividend_cash
|
.day_dividend_cash
|
||||||
@@ -545,6 +574,11 @@ impl Position {
|
|||||||
self.lots = scaled_lots;
|
self.lots = scaled_lots;
|
||||||
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
|
self.quantity = self.lots.iter().map(|lot| lot.quantity).sum();
|
||||||
self.last_price /= ratio;
|
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 {
|
if self.average_cost.is_finite() && self.average_cost > 0.0 {
|
||||||
self.average_cost /= ratio;
|
self.average_cost /= ratio;
|
||||||
} else {
|
} else {
|
||||||
@@ -1128,8 +1162,11 @@ impl PortfolioState {
|
|||||||
.map(|position| {
|
.map(|position| {
|
||||||
let market_value = position.market_value();
|
let market_value = position.market_value();
|
||||||
let entry_average_cost = position
|
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)
|
.filter(|value| value.is_finite() && *value > 0.0)
|
||||||
|
.or_else(|| position.average_entry_price())
|
||||||
.unwrap_or(position.average_cost);
|
.unwrap_or(position.average_cost);
|
||||||
HoldingSummary {
|
HoldingSummary {
|
||||||
date,
|
date,
|
||||||
@@ -1181,6 +1218,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 old_average_price = old_position.average_price;
|
||||||
let old_average_cost = old_position.average_cost;
|
let old_average_cost = old_position.average_cost;
|
||||||
let realized_pnl = old_position.realized_pnl;
|
let realized_pnl = old_position.realized_pnl;
|
||||||
let realized_entry_pnl = old_position.realized_entry_pnl;
|
let realized_entry_pnl = old_position.realized_entry_pnl;
|
||||||
@@ -1218,6 +1256,7 @@ impl PortfolioState {
|
|||||||
.entry(new_symbol.to_string())
|
.entry(new_symbol.to_string())
|
||||||
.or_insert_with(|| Position::new(new_symbol));
|
.or_insert_with(|| Position::new(new_symbol));
|
||||||
let successor_quantity_before = successor.quantity;
|
let successor_quantity_before = successor.quantity;
|
||||||
|
let successor_average_price_before = successor.average_price;
|
||||||
let successor_average_cost_before = successor.average_cost;
|
let successor_average_cost_before = successor.average_cost;
|
||||||
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();
|
||||||
@@ -1232,6 +1271,30 @@ impl PortfolioState {
|
|||||||
if converted_last_price > 0.0 {
|
if converted_last_price > 0.0 {
|
||||||
successor.last_price = converted_last_price;
|
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()
|
let converted_average_cost = if old_average_cost.is_finite()
|
||||||
&& old_average_cost > 0.0
|
&& old_average_cost > 0.0
|
||||||
&& ratio.is_finite()
|
&& ratio.is_finite()
|
||||||
@@ -1375,6 +1438,7 @@ mod tests {
|
|||||||
let realized = position.sell(100, 6.0).expect("partial FIFO sell");
|
let realized = position.sell(100, 6.0).expect("partial FIFO sell");
|
||||||
|
|
||||||
assert_eq!(position.quantity, 100);
|
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_cost - 7.55).abs() < 1e-12);
|
||||||
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
||||||
assert!((realized + 405.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);
|
position.record_buy_trade_cost(22_200, 100.0);
|
||||||
|
|
||||||
assert!(position.average_cost > 5.66);
|
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.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);
|
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");
|
position.sell(2700, 16.8331).expect("partial sell");
|
||||||
|
|
||||||
assert_eq!(position.quantity, 100);
|
assert_eq!(position.quantity, 100);
|
||||||
|
assert!((position.average_price - 18.94711428571429).abs() < 1e-12);
|
||||||
assert!((position.average_cost - average_cost_before).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");
|
position.sell(100, 6.0).expect("partial sell");
|
||||||
assert_eq!(position.quantity, 100);
|
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_cost - 7.5).abs() < 1e-12);
|
||||||
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
assert!((position.average_entry_price().unwrap() - 5.0).abs() < 1e-12);
|
||||||
|
|
||||||
position.buy(date, 100, 5.0);
|
position.buy(date, 100, 5.0);
|
||||||
assert_eq!(position.quantity, 200);
|
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_cost - 6.25).abs() < 1e-12);
|
||||||
assert!((position.average_entry_price().unwrap() - 5.0).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);
|
let cash = position.apply_cash_dividend_preserve_cost_basis(0.6);
|
||||||
|
|
||||||
assert!((cash - 600.0).abs() < 1e-12);
|
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_cost - cost_before).abs() < 1e-12);
|
||||||
assert!((position.average_entry_price().unwrap() - (entry_before - 0.6)).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);
|
assert!((position.last_price - 45.85).abs() < 1e-12);
|
||||||
|
|||||||
Reference in New Issue
Block a user