优化回测撮合与涨跌停约束

This commit is contained in:
boris
2026-06-20 07:59:22 +08:00
parent 5ecb0e7986
commit 5e66e9799c
3 changed files with 863 additions and 83 deletions
+229 -11
View File
@@ -2030,6 +2030,34 @@ where
}
}
fn aiquant_order_limit_check_price(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
algo_request: Option<&AlgoExecutionRequest>,
) -> f64 {
let matching_type = self.matching_type_for_algo_request(algo_request);
let start_cursor = algo_request
.and_then(|request| request.start_time)
.or(self.runtime_intraday_start_time.get())
.or(self.intraday_execution_start_time)
.map(|start_time| date.and_time(start_time));
self.latest_known_quote_at_or_before(
data.execution_quotes_on(date, symbol),
start_cursor,
snapshot,
side,
matching_type,
false,
)
.and_then(|quote| self.select_quote_reference_price(snapshot, quote, side, matching_type))
.unwrap_or_else(|| self.aiquant_limit_check_price(snapshot, side))
}
#[cfg(test)]
fn buy_rule_check(
&self,
date: NaiveDate,
@@ -2059,16 +2087,68 @@ where
RuleCheck::allow()
}
fn sell_rule_check(
fn buy_rule_check_for_order(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
candidate: &crate::data::CandidateEligibility,
instrument: Option<&Instrument>,
algo_request: Option<&AlgoExecutionRequest>,
) -> RuleCheck {
let check_price = if self.aiquant_rqalpha_execution_rules {
self.aiquant_order_limit_check_price(
date,
data,
symbol,
snapshot,
OrderSide::Buy,
algo_request,
)
} else {
ChinaAShareRiskControl::buy_check_price(snapshot, self.execution_price_field)
};
if let Some(reason) = ChinaAShareRiskControl::buy_rejection_reason(
date,
candidate,
snapshot,
instrument,
check_price,
) {
return RuleCheck::reject(reason);
}
if !self.aiquant_rqalpha_execution_rules {
return self
.rules
.can_buy(date, snapshot, candidate, self.execution_price_field);
}
RuleCheck::allow()
}
fn sell_rule_check_for_order(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
candidate: &crate::data::CandidateEligibility,
instrument: Option<&Instrument>,
position: &crate::portfolio::Position,
algo_request: Option<&AlgoExecutionRequest>,
) -> RuleCheck {
if !self.aiquant_rqalpha_execution_rules && !candidate.allow_sell {
return RuleCheck::reject("sell_disabled");
}
let check_price = if self.aiquant_rqalpha_execution_rules {
self.aiquant_limit_check_price(snapshot, OrderSide::Sell)
self.aiquant_order_limit_check_price(
date,
data,
symbol,
snapshot,
OrderSide::Sell,
algo_request,
)
} else {
ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field)
};
@@ -2116,8 +2196,16 @@ where
let Ok(candidate) = data.require_candidate(date, symbol) else {
return current_qty;
};
let rule =
self.sell_rule_check(date, snapshot, candidate, data.instrument(symbol), position);
let rule = self.sell_rule_check_for_order(
date,
data,
symbol,
snapshot,
candidate,
data.instrument(symbol),
position,
None,
);
if !rule.allowed {
return current_qty;
}
@@ -2155,7 +2243,15 @@ where
let Ok(candidate) = data.require_candidate(date, symbol) else {
return current_qty;
};
let rule = self.buy_rule_check(date, snapshot, candidate, data.instrument(symbol));
let rule = self.buy_rule_check_for_order(
date,
data,
symbol,
snapshot,
candidate,
data.instrument(symbol),
None,
);
if !rule.allowed {
return current_qty;
}
@@ -2199,8 +2295,16 @@ 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.sell_rule_check(date, snapshot, candidate, data.instrument(symbol), position);
let rule = self.sell_rule_check_for_order(
date,
data,
symbol,
snapshot,
candidate,
data.instrument(symbol),
position,
None,
);
if !rule.allowed {
return rule.reason;
}
@@ -2240,7 +2344,15 @@ where
) -> Option<String> {
let snapshot = data.require_market(date, symbol).ok()?;
let candidate = data.require_candidate(date, symbol).ok()?;
let rule = self.buy_rule_check(date, snapshot, candidate, data.instrument(symbol));
let rule = self.buy_rule_check_for_order(
date,
data,
symbol,
snapshot,
candidate,
data.instrument(symbol),
None,
);
if !rule.allowed {
return rule.reason;
}
@@ -2310,8 +2422,16 @@ where
);
}
let rule =
self.sell_rule_check(date, snapshot, candidate, data.instrument(symbol), position);
let rule = self.sell_rule_check_for_order(
date,
data,
symbol,
snapshot,
candidate,
data.instrument(symbol),
position,
algo_request,
);
if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() {
@@ -3765,7 +3885,15 @@ where
);
}
let rule = self.buy_rule_check(date, snapshot, candidate, data.instrument(symbol));
let rule = self.buy_rule_check_for_order(
date,
data,
symbol,
snapshot,
candidate,
data.instrument(symbol),
algo_request,
);
if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() {
@@ -5773,4 +5901,94 @@ mod tests {
assert_eq!(fill.quantity, 0);
assert_eq!(fill.unfilled_reason, Some("open at or below lower limit"));
}
#[test]
fn aiquant_sell_rule_uses_intraday_quote_when_daily_snapshot_is_stale_lower_limit() {
let date = chrono::NaiveDate::from_ymd_opt(2024, 4, 17).expect("valid date");
let prev_date = chrono::NaiveDate::from_ymd_opt(2024, 4, 16).expect("valid date");
let symbol = "000001.SZ";
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_aiquant_rqalpha_execution_rules(true)
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
.with_volume_limit(false)
.with_liquidity_limit(false)
.with_inactive_limit(false)
.with_slippage_model(SlippageModel::None);
let mut snapshot = limit_test_snapshot();
snapshot.date = date;
snapshot.timestamp = Some("2024-04-17 10:18:00".to_string());
snapshot.day_open = 9.0;
snapshot.open = 9.0;
snapshot.high = 10.0;
snapshot.low = 9.0;
snapshot.close = 9.9;
snapshot.last_price = 9.0;
snapshot.bid1 = 9.0;
snapshot.ask1 = 9.0;
snapshot.prev_close = 10.0;
snapshot.upper_limit = 11.0;
snapshot.lower_limit = 9.0;
let mut candidate = limit_test_candidate(true, false);
candidate.date = date;
let mut quote = limit_test_quote(10.0, 9.99, 10.0);
quote.date = date;
quote.timestamp = date.and_hms_opt(10, 18, 0).unwrap();
quote.last_price = 10.0;
quote.bid1 = 9.99;
quote.ask1 = 10.0;
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot],
Vec::new(),
vec![candidate],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1000.0,
prev_close: 1000.0,
volume: 1_000_000,
}],
Vec::new(),
vec![quote],
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(100.0);
portfolio.position_mut(symbol).buy(prev_date, 7_200, 8.48);
let mut report = BrokerExecutionReport::default();
broker
.process_target_value(
date,
&mut portfolio,
&data,
symbol,
0.0,
"stop_loss_exit",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("target sell processed");
assert_eq!(report.fill_events.len(), 1);
assert_eq!(report.fill_events[0].quantity, 7_200);
assert!(portfolio.position(symbol).is_none());
assert!(
report
.order_events
.iter()
.all(|event| !event.reason.contains("open at or below lower limit")),
"{:?}",
report.order_events
);
}
}