修复回测撮合与AiQuant兼容语义

This commit is contained in:
boris
2026-05-18 23:06:47 +08:00
parent 3f383c1a88
commit 6e54471e57
4 changed files with 793 additions and 95 deletions

View File

@@ -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();