对齐 AiQuant RQAlpha 回测语义

This commit is contained in:
boris
2026-05-15 11:48:10 +08:00
parent 94662b6e75
commit 4577657c90
7 changed files with 1377 additions and 69 deletions

View File

@@ -1,7 +1,7 @@
use chrono::NaiveDate;
use indexmap::IndexMap;
use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use crate::data::{DataSet, DataSetError, PriceField};
@@ -205,6 +205,22 @@ impl Position {
}
}
pub fn record_buy_trade_cost(&mut self, quantity: u32, value: f64) {
if quantity == 0 || !value.is_finite() {
return;
}
let cost = value.max(0.0);
if cost <= 0.0 {
return;
}
if let Some(lot) = self.lots.last_mut() {
lot.price += cost / quantity as f64;
self.recalculate_average_cost();
}
self.day_trade_cost += cost;
self.refresh_day_pnl();
}
pub fn set_dividend_receivable(&mut self, value: f64) {
self.dividend_receivable = if value.is_finite() {
value.max(0.0)
@@ -316,6 +332,7 @@ pub struct PortfolioState {
positions: IndexMap<String, Position>,
cash_receivables: Vec<CashReceivable>,
pending_cash_flows: Vec<PendingCashFlow>,
day_sold_symbols: BTreeSet<String>,
}
#[derive(Debug, Clone)]
@@ -348,6 +365,7 @@ impl PortfolioState {
positions: IndexMap::new(),
cash_receivables: Vec::new(),
pending_cash_flows: Vec::new(),
day_sold_symbols: BTreeSet::new(),
}
}
@@ -402,7 +420,18 @@ impl PortfolioState {
}
pub fn prune_flat_positions(&mut self) {
self.positions.retain(|_, position| !position.is_flat());
let mut sold_symbols = Vec::new();
self.positions.retain(|symbol, position| {
if position.is_flat() {
if position.sold_quantity() > 0 {
sold_symbols.push(symbol.clone());
}
false
} else {
true
}
});
self.day_sold_symbols.extend(sold_symbols);
}
pub fn add_cash_receivable(&mut self, receivable: CashReceivable) {
@@ -538,6 +567,7 @@ impl PortfolioState {
}
pub fn begin_trading_day(&mut self) {
self.day_sold_symbols.clear();
for position in self.positions.values_mut() {
position.begin_trading_day();
}
@@ -550,9 +580,24 @@ impl PortfolioState {
data: &DataSet,
field: PriceField,
) -> Result<(), DataSetError> {
self.update_prices_with_options(date, data, field, false)
}
pub fn update_prices_with_options(
&mut self,
date: NaiveDate,
data: &DataSet,
field: PriceField,
same_day_buy_close_mark_at_fill: bool,
) -> Result<(), DataSetError> {
let day_sold_symbols = self.day_sold_symbols.clone();
for position in self.positions.values_mut() {
if field == PriceField::Close
let sold_today =
position.sold_quantity() > 0 || day_sold_symbols.contains(&position.symbol);
if same_day_buy_close_mark_at_fill
&& field == PriceField::Close
&& position.day_buy_quantity > 0
&& !sold_today
&& position.sellable_qty(date) == 0
&& position.last_price.is_finite()
&& position.last_price > 0.0
@@ -1165,7 +1210,7 @@ mod tests {
.expect("dataset");
portfolio
.update_prices(buy_date, &dataset, PriceField::Close)
.update_prices_with_options(buy_date, &dataset, PriceField::Close, true)
.expect("same day close");
let position = portfolio.position(symbol).expect("position");
assert!((position.last_price - 3.01).abs() < 1e-9);
@@ -1178,6 +1223,27 @@ mod tests {
let position = portfolio.position(symbol).expect("position");
assert!((position.last_price - 3.07).abs() < 1e-9);
assert!((position.market_value() - 3991.0).abs() < 1e-6);
let prev_date = NaiveDate::from_ymd_opt(2025, 2, 7).unwrap();
let mut roundtrip_portfolio = PortfolioState::new(20_000.0);
roundtrip_portfolio
.position_mut(symbol)
.buy(prev_date, 2000, 2.90);
roundtrip_portfolio.begin_trading_day();
roundtrip_portfolio
.position_mut(symbol)
.sell(2000, 3.01)
.expect("same day sell");
roundtrip_portfolio.prune_flat_positions();
roundtrip_portfolio
.position_mut(symbol)
.buy(buy_date, 1800, 3.01);
roundtrip_portfolio
.update_prices(buy_date, &dataset, PriceField::Close)
.expect("same day roundtrip close");
let position = roundtrip_portfolio.position(symbol).expect("position");
assert!((position.last_price - 3.06).abs() < 1e-9);
assert!((position.market_value() - 5508.0).abs() < 1e-6);
}
#[test]