修复AiQuant回测撮合一致性

This commit is contained in:
boris
2026-05-20 12:09:01 +08:00
parent 6e54471e57
commit db8b0bf142
7 changed files with 1327 additions and 76 deletions
+127 -32
View File
@@ -11,7 +11,7 @@ use crate::events::{
ProcessEventKind,
};
use crate::portfolio::PortfolioState;
use crate::rules::EquityRuleHooks;
use crate::rules::{EquityRuleHooks, RuleCheck};
use crate::strategy::{
AlgoOrderStyle, OpenOrderView, OrderIntent, StrategyDecision, TargetPortfolioOrderPricing,
};
@@ -111,6 +111,7 @@ pub struct BrokerSimulator<C, R> {
inactive_limit: bool,
liquidity_limit: bool,
strict_value_budget: bool,
aiquant_rqalpha_execution_rules: bool,
same_day_buy_close_mark_at_fill: bool,
intraday_execution_start_time: Option<NaiveTime>,
runtime_intraday_start_time: Cell<Option<NaiveTime>>,
@@ -133,6 +134,7 @@ impl<C, R> BrokerSimulator<C, R> {
inactive_limit: true,
liquidity_limit: true,
strict_value_budget: false,
aiquant_rqalpha_execution_rules: false,
same_day_buy_close_mark_at_fill: false,
intraday_execution_start_time: None,
runtime_intraday_start_time: Cell::new(None),
@@ -159,6 +161,7 @@ impl<C, R> BrokerSimulator<C, R> {
inactive_limit: true,
liquidity_limit: true,
strict_value_budget: false,
aiquant_rqalpha_execution_rules: false,
same_day_buy_close_mark_at_fill: false,
intraday_execution_start_time: None,
runtime_intraday_start_time: Cell::new(None),
@@ -188,6 +191,11 @@ impl<C, R> BrokerSimulator<C, R> {
self
}
pub fn with_aiquant_rqalpha_execution_rules(mut self, enabled: bool) -> Self {
self.aiquant_rqalpha_execution_rules = enabled;
self
}
pub fn with_same_day_buy_close_mark_at_fill(mut self, enabled: bool) -> Self {
self.same_day_buy_close_mark_at_fill = enabled;
self
@@ -1825,6 +1833,68 @@ where
Ok(())
}
fn aiquant_limit_check_price(
&self,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
) -> f64 {
match (self.execution_price_field, side) {
(PriceField::Last, _) => snapshot.price(PriceField::Last),
(_, OrderSide::Buy) => snapshot.buy_price(self.execution_price_field),
(_, OrderSide::Sell) => snapshot.sell_price(self.execution_price_field),
}
}
fn buy_rule_check(
&self,
date: NaiveDate,
snapshot: &crate::data::DailyMarketSnapshot,
candidate: &crate::data::CandidateEligibility,
) -> RuleCheck {
if !self.aiquant_rqalpha_execution_rules {
return self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
}
if snapshot.paused || candidate.is_paused {
return RuleCheck::reject("paused");
}
let check_price = self.aiquant_limit_check_price(snapshot, OrderSide::Buy);
if snapshot.is_at_upper_limit_price(check_price) {
return RuleCheck::reject("open at or above upper limit");
}
RuleCheck::allow()
}
fn sell_rule_check(
&self,
date: NaiveDate,
snapshot: &crate::data::DailyMarketSnapshot,
candidate: &crate::data::CandidateEligibility,
position: &crate::portfolio::Position,
) -> RuleCheck {
if !self.aiquant_rqalpha_execution_rules {
return self.rules.can_sell(
date,
snapshot,
candidate,
position,
self.execution_price_field,
);
}
if snapshot.paused || candidate.is_paused {
return RuleCheck::reject("paused");
}
let check_price = self.aiquant_limit_check_price(snapshot, OrderSide::Sell);
if snapshot.is_at_lower_limit_price(check_price) {
return RuleCheck::reject("open at or below lower limit");
}
if position.sellable_qty(date) == 0 {
return RuleCheck::reject("t+1 sellable quantity is zero");
}
RuleCheck::allow()
}
fn minimum_target_quantity(
&self,
date: NaiveDate,
@@ -1847,13 +1917,7 @@ where
let Ok(candidate) = data.require_candidate(date, symbol) else {
return current_qty;
};
let rule = self.rules.can_sell(
date,
snapshot,
candidate,
position,
self.execution_price_field,
);
let rule = self.sell_rule_check(date, snapshot, candidate, position);
if !rule.allowed {
return current_qty;
}
@@ -1891,9 +1955,7 @@ where
let Ok(candidate) = data.require_candidate(date, symbol) else {
return current_qty;
};
let rule = self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
let rule = self.buy_rule_check(date, snapshot, candidate);
if !rule.allowed {
return current_qty;
}
@@ -1937,13 +1999,7 @@ where
let position = portfolio.position(symbol)?;
let snapshot = data.require_market(date, symbol).ok()?;
let candidate = data.require_candidate(date, symbol).ok()?;
let rule = self.rules.can_sell(
date,
snapshot,
candidate,
position,
self.execution_price_field,
);
let rule = self.sell_rule_check(date, snapshot, candidate, position);
if !rule.allowed {
return rule.reason;
}
@@ -1983,9 +2039,7 @@ where
) -> Option<String> {
let snapshot = data.require_market(date, symbol).ok()?;
let candidate = data.require_candidate(date, symbol).ok()?;
let rule = self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
let rule = self.buy_rule_check(date, snapshot, candidate);
if !rule.allowed {
return rule.reason;
}
@@ -2055,13 +2109,7 @@ where
);
}
let rule = self.rules.can_sell(
date,
snapshot,
candidate,
position,
self.execution_price_field,
);
let rule = self.sell_rule_check(date, snapshot, candidate, position);
if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() {
@@ -3494,9 +3542,7 @@ where
);
}
let rule = self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
let rule = self.buy_rule_check(date, snapshot, candidate);
if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() {
@@ -4717,7 +4763,9 @@ fn sell_reason(decision: &StrategyDecision, symbol: &str) -> &'static str {
mod tests {
use super::{BrokerSimulator, MatchingType};
use crate::cost::ChinaAShareCostModel;
use crate::data::{DailyMarketSnapshot, IntradayExecutionQuote, PriceField};
use crate::data::{
CandidateEligibility, DailyMarketSnapshot, IntradayExecutionQuote, PriceField,
};
use crate::events::OrderSide;
use crate::rules::ChinaEquityRuleHooks;
@@ -4765,6 +4813,21 @@ mod tests {
}
}
fn limit_test_candidate(allow_buy: bool, allow_sell: bool) -> CandidateEligibility {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
CandidateEligibility {
date,
symbol: "000001.SZ".to_string(),
is_st: false,
is_new_listing: false,
is_paused: false,
allow_buy,
allow_sell,
is_kcb: false,
is_one_yuan: false,
}
}
#[test]
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
@@ -4849,6 +4912,38 @@ mod tests {
assert_eq!(fill.unfilled_reason, Some("open at or above upper limit"));
}
#[test]
fn aiquant_rules_allow_buy_when_day_flags_block_but_last_price_is_tradable() {
let mut snapshot = limit_test_snapshot();
snapshot.open = 11.0;
snapshot.day_open = 11.0;
snapshot.last_price = 10.98;
snapshot.ask1 = 11.0;
let candidate = limit_test_candidate(false, true);
let date = snapshot.date;
let default_broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
);
let default_rule = default_broker.buy_rule_check(date, &snapshot, &candidate);
assert!(!default_rule.allowed);
assert_eq!(
default_rule.reason.as_deref(),
Some("buy disabled by eligibility flags")
);
let aiquant_broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_aiquant_rqalpha_execution_rules(true);
let aiquant_rule = aiquant_broker.buy_rule_check(date, &snapshot, &candidate);
assert!(aiquant_rule.allowed);
}
#[test]
fn intraday_execution_rejects_sell_at_lower_limit_price() {
let broker = BrokerSimulator::new_with_execution_price(