移除策略级盘后撮合模式
This commit is contained in:
@@ -248,7 +248,6 @@ pub enum MatchingType {
|
|||||||
OpenAuction,
|
OpenAuction,
|
||||||
CurrentBarClose,
|
CurrentBarClose,
|
||||||
NextBarOpen,
|
NextBarOpen,
|
||||||
PostCloseFixedPrice,
|
|
||||||
MinuteLast,
|
MinuteLast,
|
||||||
MinuteBestOwn,
|
MinuteBestOwn,
|
||||||
MinuteBestCounterparty,
|
MinuteBestCounterparty,
|
||||||
@@ -546,12 +545,6 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn effective_remainder_policy(&self, allow_pending_limit: bool) -> RemainderPolicy {
|
fn effective_remainder_policy(&self, allow_pending_limit: bool) -> RemainderPolicy {
|
||||||
if self.matching_type == MatchingType::PostCloseFixedPrice {
|
|
||||||
return match self.runtime_time_in_force.get() {
|
|
||||||
Some(OrderTimeInForce::Fok) => RemainderPolicy::FillOrKill,
|
|
||||||
_ => RemainderPolicy::Cancel,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
match self.runtime_time_in_force.get() {
|
match self.runtime_time_in_force.get() {
|
||||||
Some(OrderTimeInForce::Fok) => RemainderPolicy::FillOrKill,
|
Some(OrderTimeInForce::Fok) => RemainderPolicy::FillOrKill,
|
||||||
Some(OrderTimeInForce::Gtc) => RemainderPolicy::KeepUntilCanceled,
|
Some(OrderTimeInForce::Gtc) => RemainderPolicy::KeepUntilCanceled,
|
||||||
@@ -990,13 +983,6 @@ where
|
|||||||
return self.clamp_execution_price(snapshot, side, raw_price);
|
return self.clamp_execution_price(snapshot, side, raw_price);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A fixed-price post-close declaration is matched at the official
|
|
||||||
// close; applying market slippage here would turn it into a different
|
|
||||||
// order contract. Fees and risk checks still run normally.
|
|
||||||
if self.matching_type == MatchingType::PostCloseFixedPrice {
|
|
||||||
return self.clamp_execution_price(snapshot, side, raw_price);
|
|
||||||
}
|
|
||||||
|
|
||||||
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
|
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
|
||||||
let mut adjusted = match self.slippage_model {
|
let mut adjusted = match self.slippage_model {
|
||||||
SlippageModel::None => raw_price,
|
SlippageModel::None => raw_price,
|
||||||
@@ -7442,7 +7428,7 @@ where
|
|||||||
MatchingType::OpenAuction
|
MatchingType::OpenAuction
|
||||||
| MatchingType::CurrentBarClose
|
| MatchingType::CurrentBarClose
|
||||||
| MatchingType::NextBarOpen
|
| MatchingType::NextBarOpen
|
||||||
| MatchingType::PostCloseFixedPrice => false,
|
=> false,
|
||||||
MatchingType::MinuteLast => self.liquidity_limit,
|
MatchingType::MinuteLast => self.liquidity_limit,
|
||||||
MatchingType::MinuteBestOwn
|
MatchingType::MinuteBestOwn
|
||||||
| MatchingType::MinuteBestCounterparty
|
| MatchingType::MinuteBestCounterparty
|
||||||
@@ -7488,7 +7474,6 @@ fn execution_price_field_from_matching_type(matching_type: MatchingType) -> Pric
|
|||||||
MatchingType::OpenAuction => PriceField::DayOpen,
|
MatchingType::OpenAuction => PriceField::DayOpen,
|
||||||
MatchingType::CurrentBarClose => PriceField::Close,
|
MatchingType::CurrentBarClose => PriceField::Close,
|
||||||
MatchingType::NextBarOpen => PriceField::Open,
|
MatchingType::NextBarOpen => PriceField::Open,
|
||||||
MatchingType::PostCloseFixedPrice => PriceField::Close,
|
|
||||||
MatchingType::MinuteLast
|
MatchingType::MinuteLast
|
||||||
| MatchingType::MinuteBestOwn
|
| MatchingType::MinuteBestOwn
|
||||||
| MatchingType::MinuteBestCounterparty
|
| MatchingType::MinuteBestCounterparty
|
||||||
@@ -7969,57 +7954,6 @@ mod tests {
|
|||||||
assert!(broker.quote_quantity_limited(MatchingType::MinuteBestCounterparty));
|
assert!(broker.quote_quantity_limited(MatchingType::MinuteBestCounterparty));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn post_close_fixed_price_uses_daily_close_without_market_slippage() {
|
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2026, 7, 6).expect("valid date");
|
|
||||||
let mut snapshot = limit_test_snapshot();
|
|
||||||
snapshot.date = date;
|
|
||||||
snapshot.timestamp = Some(format!("{date} 15:00:00"));
|
|
||||||
snapshot.close = 10.0;
|
|
||||||
snapshot.last_price = 10.0;
|
|
||||||
snapshot.bid1 = 10.0;
|
|
||||||
snapshot.ask1 = 10.0;
|
|
||||||
let mut candidate = limit_test_candidate(true, true);
|
|
||||||
candidate.date = date;
|
|
||||||
let mut benchmark = limit_test_benchmark();
|
|
||||||
benchmark.date = date;
|
|
||||||
let data = DataSet::from_components_with_actions_and_quotes(
|
|
||||||
vec![limit_test_instrument()],
|
|
||||||
vec![snapshot],
|
|
||||||
Vec::new(),
|
|
||||||
vec![candidate],
|
|
||||||
vec![benchmark],
|
|
||||||
Vec::new(),
|
|
||||||
Vec::new(),
|
|
||||||
)
|
|
||||||
.expect("valid post-close dataset");
|
|
||||||
let broker = BrokerSimulator::new(
|
|
||||||
ChinaAShareCostModel::default(),
|
|
||||||
ChinaEquityRuleHooks,
|
|
||||||
)
|
|
||||||
.with_matching_type(MatchingType::PostCloseFixedPrice)
|
|
||||||
.with_slippage_model(SlippageModel::PriceRatio(0.25))
|
|
||||||
.with_volume_limit(false)
|
|
||||||
.with_liquidity_limit(false)
|
|
||||||
.with_inactive_limit(false);
|
|
||||||
let decision = StrategyDecision {
|
|
||||||
order_intents: vec![OrderIntent::TargetValue {
|
|
||||||
symbol: "000001.SZ".to_string(),
|
|
||||||
target_value: 2_000.0,
|
|
||||||
reason: "post_close_buy".to_string(),
|
|
||||||
}],
|
|
||||||
..StrategyDecision::default()
|
|
||||||
};
|
|
||||||
let mut portfolio = PortfolioState::new(20_000.0);
|
|
||||||
let report = broker
|
|
||||||
.execute(date, &mut portfolio, &data, &decision)
|
|
||||||
.expect("post-close execution");
|
|
||||||
assert_eq!(report.fill_events.len(), 1, "report={report:?}");
|
|
||||||
assert_eq!(report.fill_events[0].price, 10.0);
|
|
||||||
assert_eq!(report.fill_events[0].quantity, 100);
|
|
||||||
assert!(!broker.has_open_orders());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_open_buy_risk_uses_execution_date_not_signal_date() {
|
fn next_open_buy_risk_uses_execution_date_not_signal_date() {
|
||||||
let signal_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let signal_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ use crate::strategy::{
|
|||||||
TargetPortfolioOrderPricing,
|
TargetPortfolioOrderPricing,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const POST_CLOSE_FIXED_PRICE_EFFECTIVE_DATE: NaiveDate =
|
|
||||||
NaiveDate::from_ymd_opt(2026, 7, 6).expect("valid post-close effective date");
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum BacktestError {
|
pub enum BacktestError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
@@ -1973,16 +1970,6 @@ where
|
|||||||
})
|
})
|
||||||
.filter(|date| self.config.end_date.map(|end| *date <= end).unwrap_or(true))
|
.filter(|date| self.config.end_date.map(|end| *date <= end).unwrap_or(true))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if self.broker.matching_type() == MatchingType::PostCloseFixedPrice
|
|
||||||
&& calendar_dates
|
|
||||||
.first()
|
|
||||||
.is_some_and(|date| *date < POST_CLOSE_FIXED_PRICE_EFFECTIVE_DATE)
|
|
||||||
{
|
|
||||||
return Err(BacktestError::Execution(format!(
|
|
||||||
"post_close_fixed_price is unavailable before {}",
|
|
||||||
POST_CLOSE_FIXED_PRICE_EFFECTIVE_DATE
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let has_decision_inputs = |date: NaiveDate| {
|
let has_decision_inputs = |date: NaiveDate| {
|
||||||
!self.data.factor_snapshot_rows_on(date).is_empty()
|
!self.data.factor_snapshot_rows_on(date).is_empty()
|
||||||
&& !self.data.candidate_snapshot_rows_on(date).is_empty()
|
&& !self.data.candidate_snapshot_rows_on(date).is_empty()
|
||||||
|
|||||||
@@ -2194,7 +2194,6 @@ impl PlatformExprStrategy {
|
|||||||
MatchingType::OpenAuction => PriceField::DayOpen,
|
MatchingType::OpenAuction => PriceField::DayOpen,
|
||||||
MatchingType::CurrentBarClose => PriceField::Close,
|
MatchingType::CurrentBarClose => PriceField::Close,
|
||||||
MatchingType::NextBarOpen => PriceField::Open,
|
MatchingType::NextBarOpen => PriceField::Open,
|
||||||
MatchingType::PostCloseFixedPrice => PriceField::Close,
|
|
||||||
MatchingType::MinuteLast
|
MatchingType::MinuteLast
|
||||||
| MatchingType::MinuteBestOwn
|
| MatchingType::MinuteBestOwn
|
||||||
| MatchingType::MinuteBestCounterparty
|
| MatchingType::MinuteBestCounterparty
|
||||||
@@ -2257,7 +2256,6 @@ impl PlatformExprStrategy {
|
|||||||
let price_field = match self.config.matching_type {
|
let price_field = match self.config.matching_type {
|
||||||
MatchingType::NextBarOpen => PriceField::Open,
|
MatchingType::NextBarOpen => PriceField::Open,
|
||||||
MatchingType::CurrentBarClose => PriceField::Close,
|
MatchingType::CurrentBarClose => PriceField::Close,
|
||||||
MatchingType::PostCloseFixedPrice => PriceField::Close,
|
|
||||||
MatchingType::OpenAuction
|
MatchingType::OpenAuction
|
||||||
| MatchingType::MinuteLast
|
| MatchingType::MinuteLast
|
||||||
| MatchingType::MinuteBestOwn
|
| MatchingType::MinuteBestOwn
|
||||||
|
|||||||
@@ -1345,10 +1345,9 @@ fn parse_matching_type(value: Option<&str>) -> Result<Option<MatchingType>, Stri
|
|||||||
match normalize_model_name(raw).as_str() {
|
match normalize_model_name(raw).as_str() {
|
||||||
"current_bar_close" => Ok(Some(MatchingType::CurrentBarClose)),
|
"current_bar_close" => Ok(Some(MatchingType::CurrentBarClose)),
|
||||||
"next_bar_open" => Ok(Some(MatchingType::NextBarOpen)),
|
"next_bar_open" => Ok(Some(MatchingType::NextBarOpen)),
|
||||||
"post_close_fixed_price" => Ok(Some(MatchingType::PostCloseFixedPrice)),
|
|
||||||
"minute_last" => Ok(Some(MatchingType::MinuteLast)),
|
"minute_last" => Ok(Some(MatchingType::MinuteLast)),
|
||||||
_ => Err(format!(
|
_ => Err(format!(
|
||||||
"matchingType only supports current_bar_close, next_bar_open, post_close_fixed_price, minute_last: {raw}"
|
"matchingType only supports current_bar_close, next_bar_open, minute_last: {raw}"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3661,7 +3660,6 @@ mod tests {
|
|||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
("current_bar_close", MatchingType::CurrentBarClose),
|
("current_bar_close", MatchingType::CurrentBarClose),
|
||||||
("next_bar_open", MatchingType::NextBarOpen),
|
("next_bar_open", MatchingType::NextBarOpen),
|
||||||
("post_close_fixed_price", MatchingType::PostCloseFixedPrice),
|
|
||||||
("minute_last", MatchingType::MinuteLast),
|
("minute_last", MatchingType::MinuteLast),
|
||||||
] {
|
] {
|
||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
@@ -3698,7 +3696,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string().contains(
|
err.to_string().contains(
|
||||||
"matchingType only supports current_bar_close, next_bar_open, post_close_fixed_price, minute_last"
|
"matchingType only supports current_bar_close, next_bar_open, minute_last"
|
||||||
),
|
),
|
||||||
"{err}"
|
"{err}"
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user