修复回测撮合与AiQuant兼容语义
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::data::{DataSet, DataSetError, PriceField};
|
||||
pub struct PositionLot {
|
||||
pub acquired_date: NaiveDate,
|
||||
pub quantity: u32,
|
||||
pub entry_price: f64,
|
||||
pub price: f64,
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ impl Position {
|
||||
self.lots.push(PositionLot {
|
||||
acquired_date: date,
|
||||
quantity,
|
||||
entry_price: price,
|
||||
price,
|
||||
});
|
||||
self.quantity += quantity;
|
||||
@@ -230,13 +232,28 @@ impl Position {
|
||||
}
|
||||
|
||||
pub fn holding_return(&self, price: f64) -> Option<f64> {
|
||||
if self.quantity == 0 || self.average_cost <= 0.0 {
|
||||
let Some(avg_price) = self.average_entry_price() else {
|
||||
return None;
|
||||
};
|
||||
if avg_price <= 0.0 {
|
||||
None
|
||||
} else {
|
||||
Some((price / self.average_cost) - 1.0)
|
||||
Some((price / avg_price) - 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn average_entry_price(&self) -> Option<f64> {
|
||||
if self.quantity == 0 {
|
||||
return None;
|
||||
}
|
||||
let total = self
|
||||
.lots
|
||||
.iter()
|
||||
.map(|lot| lot.entry_price * lot.quantity as f64)
|
||||
.sum::<f64>();
|
||||
Some(total / self.quantity as f64)
|
||||
}
|
||||
|
||||
fn recalculate_average_cost(&mut self) {
|
||||
if self.quantity == 0 {
|
||||
self.average_cost = 0.0;
|
||||
@@ -258,6 +275,7 @@ impl Position {
|
||||
}
|
||||
|
||||
for lot in &mut self.lots {
|
||||
lot.entry_price -= dividend_per_share;
|
||||
lot.price -= dividend_per_share;
|
||||
}
|
||||
self.average_cost -= dividend_per_share;
|
||||
@@ -280,6 +298,7 @@ impl Position {
|
||||
.map(|lot| PositionLot {
|
||||
acquired_date: lot.acquired_date,
|
||||
quantity: round_half_up_u32(lot.quantity as f64 * ratio),
|
||||
entry_price: lot.entry_price / ratio,
|
||||
price: lot.price / ratio,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -759,6 +778,7 @@ impl PortfolioState {
|
||||
.map(|lot| PositionLot {
|
||||
acquired_date: lot.acquired_date,
|
||||
quantity: round_half_up_u32(lot.quantity as f64 * ratio),
|
||||
entry_price: lot.entry_price / ratio,
|
||||
price: lot.price / ratio,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -855,6 +875,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strategy_entry_price_excludes_buy_commission_cost_basis() {
|
||||
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let mut position = Position::new("600561.SH");
|
||||
position.buy(date, 22_200, 5.66);
|
||||
position.record_buy_trade_cost(22_200, 100.0);
|
||||
|
||||
assert!(position.average_cost > 5.66);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portfolio_tracks_dividend_receivable_and_day_pnl() {
|
||||
let prev_date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user