对齐 AiQuant RQAlpha 回测语义
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user