优化回测撮合与涨跌停约束
This commit is contained in:
+229
-11
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user