调整回测撮合为分钟线执行价语义

This commit is contained in:
boris
2026-06-26 17:03:48 +08:00
parent 380c34aa66
commit a131c761e5
8 changed files with 59 additions and 66 deletions
+23 -26
View File
@@ -73,10 +73,9 @@ pub enum MatchingType {
OpenAuction, OpenAuction,
CurrentBarClose, CurrentBarClose,
NextBarOpen, NextBarOpen,
NextMinuteLast, MinuteLast,
NextMinuteBestOwn, MinuteBestOwn,
NextMinuteBestCounterparty, MinuteBestCounterparty,
CounterpartyOffer,
Vwap, Vwap,
Twap, Twap,
} }
@@ -563,7 +562,7 @@ where
matching_type: MatchingType, matching_type: MatchingType,
) -> Option<f64> { ) -> Option<f64> {
let raw_price = match matching_type { let raw_price = match matching_type {
MatchingType::NextMinuteBestOwn => match side { MatchingType::MinuteBestOwn => match side {
OrderSide::Buy => { OrderSide::Buy => {
if quote.bid1.is_finite() && quote.bid1 > 0.0 { if quote.bid1.is_finite() && quote.bid1 > 0.0 {
Some(quote.bid1) Some(quote.bid1)
@@ -587,13 +586,11 @@ where
} }
} }
}, },
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => { MatchingType::MinuteBestCounterparty => match side {
match side {
OrderSide::Buy => quote.buy_price(), OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(), OrderSide::Sell => quote.sell_price(),
} },
} MatchingType::MinuteLast | MatchingType::Vwap | MatchingType::Twap => {
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => {
if quote.last_price.is_finite() && quote.last_price > 0.0 { if quote.last_price.is_finite() && quote.last_price > 0.0 {
Some(quote.last_price) Some(quote.last_price)
} else { } else {
@@ -4998,7 +4995,7 @@ where
quotes, quotes,
start_cursor, start_cursor,
end_cursor, end_cursor,
matching_type == MatchingType::NextMinuteLast && start_cursor.is_some(), matching_type == MatchingType::MinuteLast && start_cursor.is_some(),
)), )),
}); });
} }
@@ -5056,12 +5053,12 @@ where
let quote_quantity_limited = let quote_quantity_limited =
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor); self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
let lot = round_lot.max(1); let lot = round_lot.max(1);
let exact_time_order_quote = matching_type != MatchingType::NextMinuteLast let exact_time_order_quote = matching_type != MatchingType::MinuteLast
&& start_cursor.is_some() && start_cursor.is_some()
&& end_cursor.is_some() && end_cursor.is_some()
&& start_cursor == end_cursor; && start_cursor == end_cursor;
let use_decision_time_quote = start_cursor.is_some() let use_decision_time_quote = start_cursor.is_some()
&& (matching_type == MatchingType::NextMinuteLast || exact_time_order_quote); && (matching_type == MatchingType::MinuteLast || exact_time_order_quote);
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote { let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
self.latest_known_quote_at_or_before( self.latest_known_quote_at_or_before(
quotes, quotes,
@@ -5290,7 +5287,7 @@ where
} }
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool { fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextMinuteLast self.volume_limit || self.liquidity_limit || matching_type != MatchingType::MinuteLast
} }
fn quote_quantity_limited_for_window( fn quote_quantity_limited_for_window(
@@ -5321,7 +5318,7 @@ fn matching_type_from_price_field(field: PriceField) -> MatchingType {
PriceField::DayOpen => MatchingType::OpenAuction, PriceField::DayOpen => MatchingType::OpenAuction,
PriceField::Open => MatchingType::NextBarOpen, PriceField::Open => MatchingType::NextBarOpen,
PriceField::Close => MatchingType::CurrentBarClose, PriceField::Close => MatchingType::CurrentBarClose,
PriceField::Last => MatchingType::NextMinuteLast, PriceField::Last => MatchingType::MinuteLast,
} }
} }
@@ -5487,17 +5484,17 @@ mod tests {
} }
#[test] #[test]
fn next_minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() { fn minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(false) .with_volume_limit(false)
.with_liquidity_limit(false); .with_liquidity_limit(false);
assert!(!broker.quote_quantity_limited(MatchingType::NextMinuteLast)); assert!(!broker.quote_quantity_limited(MatchingType::MinuteLast));
assert!(broker.quote_quantity_limited(MatchingType::CounterpartyOffer)); assert!(broker.quote_quantity_limited(MatchingType::MinuteBestCounterparty));
} }
#[test] #[test]
fn next_minute_last_keeps_quote_quantity_cap_when_limits_enabled() { fn minute_last_keeps_quote_quantity_cap_when_limits_enabled() {
let volume_limited = let volume_limited =
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(true) .with_volume_limit(true)
@@ -5507,8 +5504,8 @@ mod tests {
.with_volume_limit(false) .with_volume_limit(false)
.with_liquidity_limit(true); .with_liquidity_limit(true);
assert!(volume_limited.quote_quantity_limited(MatchingType::NextMinuteLast)); assert!(volume_limited.quote_quantity_limited(MatchingType::MinuteLast));
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextMinuteLast)); assert!(liquidity_limited.quote_quantity_limited(MatchingType::MinuteLast));
} }
#[test] #[test]
@@ -5558,7 +5555,7 @@ mod tests {
PriceField::Last, PriceField::Last,
) )
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time()) .with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
.with_matching_type(MatchingType::NextMinuteLast) .with_matching_type(MatchingType::MinuteLast)
.with_slippage_model(SlippageModel::PriceRatio(0.001)) .with_slippage_model(SlippageModel::PriceRatio(0.001))
.with_volume_limit(false) .with_volume_limit(false)
.with_liquidity_limit(false); .with_liquidity_limit(false);
@@ -5751,7 +5748,7 @@ mod tests {
} }
#[test] #[test]
fn next_minute_last_execution_uses_latest_quote_before_decision_time() { fn minute_last_execution_uses_latest_quote_before_decision_time() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let broker = BrokerSimulator::new_with_execution_price( let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(), ChinaAShareCostModel::default(),
@@ -5772,7 +5769,7 @@ mod tests {
&snapshot, &snapshot,
&[quote], &[quote],
OrderSide::Sell, OrderSide::Sell,
MatchingType::NextMinuteLast, MatchingType::MinuteLast,
Some(decision_time), Some(decision_time),
Some(decision_time), Some(decision_time),
200, 200,
@@ -6026,7 +6023,7 @@ mod tests {
&snapshot, &snapshot,
&[quote], &[quote],
OrderSide::Buy, OrderSide::Buy,
MatchingType::NextMinuteLast, MatchingType::MinuteLast,
Some(start), Some(start),
None, None,
100, 100,
@@ -6103,7 +6100,7 @@ mod tests {
&snapshot, &snapshot,
&[quote], &[quote],
OrderSide::Sell, OrderSide::Sell,
MatchingType::NextMinuteLast, MatchingType::MinuteLast,
Some(start), Some(start),
None, None,
100, 100,
+4 -6
View File
@@ -1444,7 +1444,7 @@ where
let snapshot = self.data.market(date, &intent.symbol); let snapshot = self.data.market(date, &intent.symbol);
if matches!( if matches!(
self.broker.matching_type(), self.broker.matching_type(),
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer MatchingType::MinuteBestCounterparty
) { ) {
let depth = self.data.order_book_depth_on(date, &intent.symbol); let depth = self.data.order_book_depth_on(date, &intent.symbol);
if !depth.is_empty() { if !depth.is_empty() {
@@ -1454,7 +1454,7 @@ where
let quotes = self.data.execution_quotes_on(date, &intent.symbol); let quotes = self.data.execution_quotes_on(date, &intent.symbol);
for quote in quotes { for quote in quotes {
let price = match self.broker.matching_type() { let price = match self.broker.matching_type() {
MatchingType::NextMinuteBestOwn => match intent.side() { MatchingType::MinuteBestOwn => match intent.side() {
OrderSide::Buy => { OrderSide::Buy => {
if quote.bid1.is_finite() && quote.bid1 > 0.0 { if quote.bid1.is_finite() && quote.bid1 > 0.0 {
quote.bid1 quote.bid1
@@ -1470,12 +1470,10 @@ where
} }
} }
}, },
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => { MatchingType::MinuteBestCounterparty => match intent.side() {
match intent.side() {
OrderSide::Buy => quote.buy_price().unwrap_or(quote.last_price), OrderSide::Buy => quote.buy_price().unwrap_or(quote.last_price),
OrderSide::Sell => quote.sell_price().unwrap_or(quote.last_price), OrderSide::Sell => quote.sell_price().unwrap_or(quote.last_price),
} },
}
_ => quote.last_price, _ => quote.last_price,
}; };
if let Some(snapshot) = snapshot { if let Some(snapshot) = snapshot {
+11 -12
View File
@@ -286,7 +286,7 @@ fn band_low(index_close) {
stamp_tax_rate_after_change: None, stamp_tax_rate_after_change: None,
strict_value_budget: false, strict_value_budget: false,
slippage_model: SlippageModel::None, slippage_model: SlippageModel::None,
matching_type: MatchingType::NextMinuteLast, matching_type: MatchingType::MinuteLast,
quote_quantity_limit: true, quote_quantity_limit: true,
current_day_precomputed_factors: false, current_day_precomputed_factors: false,
prefer_precomputed_rolling_factors: false, prefer_precomputed_rolling_factors: false,
@@ -1342,7 +1342,7 @@ impl PlatformExprStrategy {
let last = let last =
|| (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price); || (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price);
match self.config.matching_type { match self.config.matching_type {
MatchingType::NextMinuteBestOwn => match side { MatchingType::MinuteBestOwn => match side {
OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0) OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0)
.then_some(quote.bid1) .then_some(quote.bid1)
.or_else(last), .or_else(last),
@@ -1350,17 +1350,16 @@ impl PlatformExprStrategy {
.then_some(quote.ask1) .then_some(quote.ask1)
.or_else(last), .or_else(last),
}, },
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => { MatchingType::MinuteBestCounterparty => match side {
match side {
OrderSide::Buy => quote.buy_price(), OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(), OrderSide::Sell => quote.sell_price(),
} },
} MatchingType::MinuteLast | MatchingType::Vwap | MatchingType::Twap => {
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => last() last().or_else(|| match side {
.or_else(|| match side {
OrderSide::Buy => quote.buy_price(), OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(), OrderSide::Sell => quote.sell_price(),
}), })
}
_ => match side { _ => match side {
OrderSide::Buy => quote.buy_price(), OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(), OrderSide::Sell => quote.sell_price(),
@@ -8423,7 +8422,7 @@ mod tests {
} }
#[test] #[test]
fn platform_aiquant_next_minute_last_projection_uses_last_quote_for_buy_budget() { fn platform_aiquant_minute_last_projection_uses_last_quote_for_buy_budget() {
let date = d(2023, 5, 4); let date = d(2023, 5, 4);
let symbol = "000782.SZ"; let symbol = "000782.SZ";
let data = DataSet::from_components_with_actions_and_quotes( let data = DataSet::from_components_with_actions_and_quotes(
@@ -8530,7 +8529,7 @@ mod tests {
cfg.minimum_commission = Some(5.0); cfg.minimum_commission = Some(5.0);
cfg.strict_value_budget = true; cfg.strict_value_budget = true;
cfg.slippage_model = SlippageModel::PriceRatio(0.002); cfg.slippage_model = SlippageModel::PriceRatio(0.002);
cfg.matching_type = MatchingType::NextMinuteLast; cfg.matching_type = MatchingType::MinuteLast;
cfg.quote_quantity_limit = false; cfg.quote_quantity_limit = false;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time")); cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
let strategy = PlatformExprStrategy::new(cfg); let strategy = PlatformExprStrategy::new(cfg);
@@ -8688,7 +8687,7 @@ mod tests {
cfg.minimum_commission = Some(5.0); cfg.minimum_commission = Some(5.0);
cfg.strict_value_budget = true; cfg.strict_value_budget = true;
cfg.slippage_model = SlippageModel::PriceRatio(0.002); cfg.slippage_model = SlippageModel::PriceRatio(0.002);
cfg.matching_type = MatchingType::NextMinuteLast; cfg.matching_type = MatchingType::MinuteLast;
cfg.quote_quantity_limit = false; cfg.quote_quantity_limit = false;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time")); cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
let strategy = PlatformExprStrategy::new(cfg); let strategy = PlatformExprStrategy::new(cfg);
@@ -423,10 +423,9 @@ fn parse_matching_type(value: Option<&str>) -> Option<MatchingType> {
"open_auction" => Some(MatchingType::OpenAuction), "open_auction" => Some(MatchingType::OpenAuction),
"current_bar_close" => Some(MatchingType::CurrentBarClose), "current_bar_close" => Some(MatchingType::CurrentBarClose),
"next_bar_open" => Some(MatchingType::NextBarOpen), "next_bar_open" => Some(MatchingType::NextBarOpen),
"next_minute_last" => Some(MatchingType::NextMinuteLast), "minute_last" => Some(MatchingType::MinuteLast),
"next_minute_best_own" => Some(MatchingType::NextMinuteBestOwn), "minute_best_own" => Some(MatchingType::MinuteBestOwn),
"next_minute_best_counterparty" => Some(MatchingType::NextMinuteBestCounterparty), "minute_best_counterparty" => Some(MatchingType::MinuteBestCounterparty),
"counterparty_offer" => Some(MatchingType::CounterpartyOffer),
"vwap" => Some(MatchingType::Vwap), "vwap" => Some(MatchingType::Vwap),
"twap" => Some(MatchingType::Twap), "twap" => Some(MatchingType::Twap),
_ => None, _ => None,
@@ -1655,7 +1654,7 @@ mod tests {
fn parses_execution_slippage_overrides_into_platform_config() { fn parses_execution_slippage_overrides_into_platform_config() {
let spec = serde_json::json!({ let spec = serde_json::json!({
"execution": { "execution": {
"matchingType": "next_minute_last", "matchingType": "minute_last",
"slippageModel": "price_ratio", "slippageModel": "price_ratio",
"slippageValue": 0.001, "slippageValue": 0.001,
"strictValueBudget": true "strictValueBudget": true
@@ -1671,7 +1670,7 @@ mod tests {
let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!(cfg.matching_type, MatchingType::NextMinuteLast); assert_eq!(cfg.matching_type, MatchingType::MinuteLast);
assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001)); assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001));
assert!(cfg.strict_value_budget); assert!(cfg.strict_value_budget);
} }
+4 -4
View File
@@ -220,7 +220,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
}, },
ManualSection { ManualSection {
title: "execution.matching_type / execution.slippage".to_string(), title: "execution.matching_type / execution.slippage".to_string(),
detail: "设置撮合模式和滑点。使用 execution.matching_type(\"next_minute_last\" | \"next_minute_best_own\" | \"next_minute_best_counterparty\" | \"counterparty_offer\" | \"vwap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。next_minute_last 使用分钟执行价 last_pricenext_minute_best_own / next_minute_best_counterparty 会按 L1 买一卖一近似 平台内核 的最优价语义;counterparty_offer 在存在 order_book_depth 多档盘口数据时按真实档位逐档扫单并计算加权成交价,不存在 depth 时回退 L1 对手方报价;vwap 会在盘中执行价链路上聚合多笔成交为单条 VWAP 成交;next_bar_open 使用决策日信号并在下一可交易日开盘撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(), detail: "设置撮合模式和滑点。使用 execution.matching_type(\"minute_last\" | \"minute_best_own\" | \"minute_best_counterparty\" | \"vwap\" | \"twap\" | \"current_bar_close\" | \"next_bar_open\" | \"open_auction\")。minute_last 使用分钟执行价 last_priceminute_best_own 使用己方一档报价,minute_best_counterparty 使用对手方一档报价,存在 order_book_depth 多档盘口数据时按真实档位逐档扫单;vwap/twap 在分钟执行价窗口内聚合成交;next_bar_open 使用决策日信号并在下一可交易日开盘撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;open_auction 使用当日集合竞价开盘价 day_open 进行撮合,且不额外施加滑点,并按竞价成交量而不是盘口一档流动性限制成交;滑点支持 execution.slippage(\"none\") / execution.slippage(\"price_ratio\", 0.001) / execution.slippage(\"tick_size\", 1) / execution.slippage(\"limit_price\"),其中 limit_price 会在限价单成交时按挂单价模拟 平台内核 的最坏成交价。".to_string(),
}, },
ManualSection { ManualSection {
title: "期货提交校验".to_string(), title: "期货提交校验".to_string(),
@@ -360,7 +360,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
}, },
ManualFactorSource { ManualFactorSource {
table: "盘口深度参数".to_string(), table: "盘口深度参数".to_string(),
detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 counterparty_offer / next_minute_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(), detail: "可选字段包括 date、symbol、timestamp、level、bid_price、bid_volume、ask_price、ask_volume。存在盘口深度时,期货 minute_best_counterparty 可按真实多档盘口逐档扫单;不存在时不会伪造 depth。".to_string(),
fields: vec![], fields: vec![],
}, },
ManualFactorSource { ManualFactorSource {
@@ -384,7 +384,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
}, },
ManualExample { ManualExample {
title: "分钟执行价撮合 + 最小价位滑点".to_string(), title: "分钟执行价撮合 + 最小价位滑点".to_string(),
code: "execution.matching_type(\"next_minute_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(), code: "execution.matching_type(\"minute_last\")\nexecution.slippage(\"tick_size\", 1)".to_string(),
}, },
ManualExample { ManualExample {
title: "动态 universe 和订阅".to_string(), title: "动态 universe 和订阅".to_string(),
@@ -518,7 +518,7 @@ pub fn build_generation_prompt(
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n"); prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\n");
prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n"); prompt.push_str("- 不要使用手册未列出的字段、函数或外部平台 API 名称。\n\n");
prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、execution.matching_type、execution.slippage、universe.exclude。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n"); prompt.push_str("只允许使用这些可编译语句:market、benchmark、signal、rebalance.every_days(...).at([...])、selection.limit、selection.market_cap_band、filter.stock_ma、filter.stock_expr、ordering.rank_by、ordering.rank_expr、allocation.buy_scale、risk.stop_loss、risk.take_profit、risk.index_exposure、execution.matching_type、execution.slippage、universe.exclude。禁止输出 filter(...)、rank(...)、select.top(...)、weight.equal()、sell_rule(...)、backtest(...)、risk.max_position(...) 这类未支持伪语法。\n");
prompt.push_str("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma6060日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,写 !is_st、!paused、!at_upper_limit、!at_lower_limit,不要写 is_st == 0risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;execution.matching_type 只能取 next_minute_last、next_minute_best_own、next_minute_best_counterparty、counterparty_offer、vwap、current_bar_close、next_bar_open、open_auctionnext_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n"); prompt.push_str("参数形态必须严格:selection.market_cap_band 必须写 field=\"market_cap\" 或 field=\"free_float_cap\", lower=..., upper=...;禁止使用 float_market_cap;禁止使用 ma60、stock_ma60、signal_ma60、benchmark_ma6060日价格均线写 rolling_mean(\"close\", 60) 或 ma(\"close\", 60),任意窗口均量写 rolling_mean(\"volume\", n) 或 vma(n);不要生成 fn score() 这类零参数函数,股票字段排序直接写在 ordering.rank_expr 内或用带参数函数;布尔字段按布尔使用,写 !is_st、!paused、!at_upper_limit、!at_lower_limit,不要写 is_st == 0risk.index_exposure 只能传一个数值表达式,不要使用 risk.exposure;完整三元表达式 cond ? a : b 可以使用,但不得输出残缺问号/冒号片段;execution.matching_type 只能取 minute_last、minute_best_own、minute_best_counterparty、vwap、twap、current_bar_close、next_bar_open、open_auctionnext_bar_open 只能使用决策日信号,不能把执行日价格当作下单前信息;execution.slippage 必须写 execution.slippage(\"none\") 或 execution.slippage(\"price_ratio\", 0.001)。\n");
prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n"); prompt.push_str("回测成功但 tradeCount=0 或 holdingCount=0 是无效策略;第一版必须保持稳定买入覆盖率,复杂因子只能在后续优化中逐步加严。\n");
prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && !is_st && !paused && close > 2 && !at_upper_limit && !at_lower_limit)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n"); prompt.push_str("可参考但不要照抄的最小模板,回复时不要包含 ``` 代码围栏:\nstrategy(\"cn_a_smallcap_factor_rotation\") {\nmarket(\"CN_A\")\nbenchmark(\"000852.SH\")\nsignal(\"000001.SH\")\nrebalance.every_days(5).at([\"10:18\"])\nselection.limit(40)\nselection.market_cap_band(field=\"market_cap\", lower=0, upper=1000)\nfilter.stock_expr(listed_days >= 60 && !is_st && !paused && close > 2 && !at_upper_limit && !at_lower_limit)\nordering.rank_by(\"market_cap\", \"asc\")\nallocation.buy_scale(1.0)\nrisk.index_exposure(1.0)\nrisk.stop_loss(holding_return < -0.08)\nexecution.slippage(\"price_ratio\", 0.001)\n}\n\n");
prompt.push_str("用户目标:\n"); prompt.push_str("用户目标:\n");
@@ -190,7 +190,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
ChinaEquityRuleHooks, ChinaEquityRuleHooks,
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::NextMinuteLast) .with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0)); .with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig { let config = BacktestConfig {
initial_cash: 10_000.0, initial_cash: 10_000.0,
@@ -385,7 +385,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
ChinaEquityRuleHooks, ChinaEquityRuleHooks,
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::NextMinuteLast) .with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0)); .with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig { let config = BacktestConfig {
initial_cash: 10_000.0, initial_cash: 10_000.0,
@@ -587,7 +587,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
ChinaEquityRuleHooks, ChinaEquityRuleHooks,
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::NextMinuteLast) .with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0)); .with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig { let config = BacktestConfig {
initial_cash: 10_000.0, initial_cash: 10_000.0,
+1 -1
View File
@@ -1842,7 +1842,7 @@ fn engine_sweeps_futures_order_book_depth_when_available() {
ChinaEquityRuleHooks::default(), ChinaEquityRuleHooks::default(),
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::CounterpartyOffer); .with_matching_type(MatchingType::MinuteBestCounterparty);
let mut engine = BacktestEngine::new( let mut engine = BacktestEngine::new(
data, data,
FuturesDepthLimitOrderStrategy, FuturesDepthLimitOrderStrategy,
@@ -349,7 +349,7 @@ fn broker_delayed_limit_open_sell_uses_minute_price() {
ChinaEquityRuleHooks::default(), ChinaEquityRuleHooks::default(),
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::NextMinuteLast) .with_matching_type(MatchingType::MinuteLast)
.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 31, 0).unwrap()) .with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 31, 0).unwrap())
.with_volume_limit(false) .with_volume_limit(false)
.with_liquidity_limit(false); .with_liquidity_limit(false);
@@ -2888,7 +2888,7 @@ fn broker_uses_best_own_price_for_intraday_matching() {
ChinaEquityRuleHooks::default(), ChinaEquityRuleHooks::default(),
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::NextMinuteBestOwn); .with_matching_type(MatchingType::MinuteBestOwn);
let report = broker let report = broker
.execute( .execute(
@@ -3003,7 +3003,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() {
ChinaEquityRuleHooks::default(), ChinaEquityRuleHooks::default(),
PriceField::Last, PriceField::Last,
) )
.with_matching_type(MatchingType::NextMinuteBestCounterparty); .with_matching_type(MatchingType::MinuteBestCounterparty);
let report = broker let report = broker
.execute( .execute(