修复AiQuant回测撮合一致性
This commit is contained in:
+127
-32
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user