chore: 更新 fidc-backtest-engine - 2026-05-22

This commit is contained in:
boris
2026-05-22 17:22:33 +08:00
parent 7dbd66b467
commit 3499d4aa74
8 changed files with 526 additions and 125 deletions
+60 -30
View File
@@ -10,7 +10,9 @@ use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::risk_control::ChinaAShareRiskControl;
use crate::rules::{EquityRuleHooks, RuleCheck};
use crate::strategy::{
AlgoOrderStyle, OpenOrderView, OrderIntent, StrategyDecision, TargetPortfolioOrderPricing,
@@ -1850,19 +1852,27 @@ where
date: NaiveDate,
snapshot: &crate::data::DailyMarketSnapshot,
candidate: &crate::data::CandidateEligibility,
instrument: Option<&Instrument>,
) -> RuleCheck {
let check_price = if self.aiquant_rqalpha_execution_rules {
self.aiquant_limit_check_price(snapshot, OrderSide::Buy)
} 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);
}
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()
}
@@ -1871,8 +1881,24 @@ where
date: NaiveDate,
snapshot: &crate::data::DailyMarketSnapshot,
candidate: &crate::data::CandidateEligibility,
instrument: Option<&Instrument>,
position: &crate::portfolio::Position,
) -> RuleCheck {
let check_price = if self.aiquant_rqalpha_execution_rules {
self.aiquant_limit_check_price(snapshot, OrderSide::Sell)
} else {
ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field)
};
if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason(
date,
candidate,
snapshot,
instrument,
Some(position),
check_price,
) {
return RuleCheck::reject(reason);
}
if !self.aiquant_rqalpha_execution_rules {
return self.rules.can_sell(
date,
@@ -1882,16 +1908,6 @@ where
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()
}
@@ -1917,7 +1933,8 @@ where
let Ok(candidate) = data.require_candidate(date, symbol) else {
return current_qty;
};
let rule = self.sell_rule_check(date, snapshot, candidate, position);
let rule =
self.sell_rule_check(date, snapshot, candidate, data.instrument(symbol), position);
if !rule.allowed {
return current_qty;
}
@@ -1955,7 +1972,7 @@ where
let Ok(candidate) = data.require_candidate(date, symbol) else {
return current_qty;
};
let rule = self.buy_rule_check(date, snapshot, candidate);
let rule = self.buy_rule_check(date, snapshot, candidate, data.instrument(symbol));
if !rule.allowed {
return current_qty;
}
@@ -1999,7 +2016,8 @@ 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, position);
let rule =
self.sell_rule_check(date, snapshot, candidate, data.instrument(symbol), position);
if !rule.allowed {
return rule.reason;
}
@@ -2039,7 +2057,7 @@ 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);
let rule = self.buy_rule_check(date, snapshot, candidate, data.instrument(symbol));
if !rule.allowed {
return rule.reason;
}
@@ -2109,12 +2127,15 @@ where
);
}
let rule = self.sell_rule_check(date, snapshot, candidate, position);
let rule =
self.sell_rule_check(date, snapshot, candidate, data.instrument(symbol), position);
if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() {
Some("paused")
| Some("sell disabled by eligibility flags")
| Some("sell_disabled")
| Some("lower_limit")
| Some("open at or below lower limit") => OrderStatus::Canceled,
_ => OrderStatus::Rejected,
};
@@ -3542,12 +3563,14 @@ where
);
}
let rule = self.buy_rule_check(date, snapshot, candidate);
let rule = self.buy_rule_check(date, snapshot, candidate, data.instrument(symbol));
if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() {
Some("paused")
| Some("buy disabled by eligibility flags")
| Some("buy_disabled")
| Some("upper_limit")
| Some("open at or above upper limit") => OrderStatus::Canceled,
_ => OrderStatus::Rejected,
};
@@ -4721,6 +4744,8 @@ fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
| "tick volume limit"
| "intraday quote liquidity exhausted"
| "no execution quotes after start"
| "upper_limit"
| "lower_limit"
| "open at or above upper limit"
| "open at or below lower limit" => OrderStatus::Canceled,
_ => OrderStatus::Rejected,
@@ -4733,6 +4758,8 @@ fn final_partial_fill_status(partial_reason: Option<&str>) -> OrderStatus {
if reason.contains("market liquidity or volume limit")
|| reason.contains("intraday quote liquidity exhausted")
|| reason.contains("no execution quotes after start")
|| reason.contains("upper_limit")
|| reason.contains("lower_limit")
|| reason.contains("open at or above upper limit")
|| reason.contains("open at or below lower limit") =>
{
@@ -4913,7 +4940,7 @@ mod tests {
}
#[test]
fn aiquant_rules_allow_buy_when_day_flags_block_but_last_price_is_tradable() {
fn aiquant_rules_keep_structured_buy_risk_while_using_aiquant_limit_price() {
let mut snapshot = limit_test_snapshot();
snapshot.open = 11.0;
snapshot.day_open = 11.0;
@@ -4927,12 +4954,9 @@ mod tests {
ChinaEquityRuleHooks,
PriceField::Last,
);
let default_rule = default_broker.buy_rule_check(date, &snapshot, &candidate);
let default_rule = default_broker.buy_rule_check(date, &snapshot, &candidate, None);
assert!(!default_rule.allowed);
assert_eq!(
default_rule.reason.as_deref(),
Some("buy disabled by eligibility flags")
);
assert_eq!(default_rule.reason.as_deref(), Some("trade_disabled"));
let aiquant_broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
@@ -4940,7 +4964,13 @@ mod tests {
PriceField::Last,
)
.with_aiquant_rqalpha_execution_rules(true);
let aiquant_rule = aiquant_broker.buy_rule_check(date, &snapshot, &candidate);
let aiquant_rule = aiquant_broker.buy_rule_check(date, &snapshot, &candidate, None);
assert!(!aiquant_rule.allowed);
assert_eq!(aiquant_rule.reason.as_deref(), Some("trade_disabled"));
let tradable_candidate = limit_test_candidate(true, true);
let aiquant_rule =
aiquant_broker.buy_rule_check(date, &snapshot, &tradable_candidate, None);
assert!(aiquant_rule.allowed);
}