切换回测执行价为分钟线语义

This commit is contained in:
boris
2026-06-26 13:27:49 +08:00
parent 6db480b91d
commit 7f40cfdab0
14 changed files with 129 additions and 132 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
## 当前能力
- 日频和分钟执行价策略生命周期与确定性回放;旧 tick 入口仅作为兼容别名映射到分钟执行价
- 日频和分钟执行价策略生命周期与确定性回放。
- A 股行情、估值、因子、基准、候选资格、涨跌停触达、停牌和 ST 标记。
- 平台策略 DSL 与 `StrategyContext` 数据 API,不暴露非平台脚本语法。
- `BacktestConfig` 支持起止日期、初始资金、决策滞后、执行价格字段、基准代码。
+26 -26
View File
@@ -73,9 +73,9 @@ pub enum MatchingType {
OpenAuction,
CurrentBarClose,
NextBarOpen,
NextTickLast,
NextTickBestOwn,
NextTickBestCounterparty,
NextMinuteLast,
NextMinuteBestOwn,
NextMinuteBestCounterparty,
CounterpartyOffer,
Vwap,
Twap,
@@ -563,7 +563,7 @@ where
matching_type: MatchingType,
) -> Option<f64> {
let raw_price = match matching_type {
MatchingType::NextTickBestOwn => match side {
MatchingType::NextMinuteBestOwn => match side {
OrderSide::Buy => {
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
Some(quote.bid1)
@@ -587,13 +587,13 @@ where
}
}
},
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
match side {
OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(),
}
}
MatchingType::NextTickLast | MatchingType::Vwap | MatchingType::Twap => {
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => {
if quote.last_price.is_finite() && quote.last_price > 0.0 {
Some(quote.last_price)
} else {
@@ -4806,7 +4806,7 @@ where
}
if self.inactive_limit && snapshot.tick_volume == 0 {
return Err("tick no volume".to_string());
return Err("minute no volume".to_string());
}
let mut max_fill = requested_qty;
@@ -4835,7 +4835,7 @@ where
let raw_limit = ((snapshot.tick_volume as f64) * self.volume_percent).round() as i64
- consumed_turnover as i64;
if raw_limit <= 0 {
return Err("tick volume limit".to_string());
return Err("minute volume limit".to_string());
}
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
raw_limit as u32
@@ -4843,7 +4843,7 @@ where
self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
};
if volume_limited == 0 {
return Err("tick volume limit".to_string());
return Err("minute volume limit".to_string());
}
max_fill = max_fill.min(volume_limited);
}
@@ -4998,7 +4998,7 @@ where
quotes,
start_cursor,
end_cursor,
matching_type == MatchingType::NextTickLast && start_cursor.is_some(),
matching_type == MatchingType::NextMinuteLast && start_cursor.is_some(),
)),
});
}
@@ -5056,12 +5056,12 @@ where
let quote_quantity_limited =
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
let lot = round_lot.max(1);
let exact_time_order_quote = matching_type != MatchingType::NextTickLast
let exact_time_order_quote = matching_type != MatchingType::NextMinuteLast
&& start_cursor.is_some()
&& end_cursor.is_some()
&& start_cursor == end_cursor;
let use_decision_time_quote = start_cursor.is_some()
&& (matching_type == MatchingType::NextTickLast || exact_time_order_quote);
&& (matching_type == MatchingType::NextMinuteLast || exact_time_order_quote);
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
self.latest_known_quote_at_or_before(
quotes,
@@ -5290,7 +5290,7 @@ where
}
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextTickLast
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextMinuteLast
}
fn quote_quantity_limited_for_window(
@@ -5321,7 +5321,7 @@ fn matching_type_from_price_field(field: PriceField) -> MatchingType {
PriceField::DayOpen => MatchingType::OpenAuction,
PriceField::Open => MatchingType::NextBarOpen,
PriceField::Close => MatchingType::CurrentBarClose,
PriceField::Last => MatchingType::NextTickLast,
PriceField::Last => MatchingType::NextMinuteLast,
}
}
@@ -5338,8 +5338,8 @@ fn merge_partial_fill_reason(current: Option<String>, next: Option<&str>) -> Opt
fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
match reason {
"tick no volume"
| "tick volume limit"
"minute no volume"
| "minute volume limit"
| "intraday quote liquidity exhausted"
| "no execution quotes at or before start"
| "no execution quotes after start"
@@ -5487,17 +5487,17 @@ mod tests {
}
#[test]
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
fn next_minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(false)
.with_liquidity_limit(false);
assert!(!broker.quote_quantity_limited(MatchingType::NextTickLast));
assert!(!broker.quote_quantity_limited(MatchingType::NextMinuteLast));
assert!(broker.quote_quantity_limited(MatchingType::CounterpartyOffer));
}
#[test]
fn next_tick_last_keeps_quote_quantity_cap_when_limits_enabled() {
fn next_minute_last_keeps_quote_quantity_cap_when_limits_enabled() {
let volume_limited =
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(true)
@@ -5507,8 +5507,8 @@ mod tests {
.with_volume_limit(false)
.with_liquidity_limit(true);
assert!(volume_limited.quote_quantity_limited(MatchingType::NextTickLast));
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextTickLast));
assert!(volume_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
}
#[test]
@@ -5558,7 +5558,7 @@ mod tests {
PriceField::Last,
)
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
.with_matching_type(MatchingType::NextTickLast)
.with_matching_type(MatchingType::NextMinuteLast)
.with_slippage_model(SlippageModel::PriceRatio(0.001))
.with_volume_limit(false)
.with_liquidity_limit(false);
@@ -5751,7 +5751,7 @@ mod tests {
}
#[test]
fn next_tick_last_execution_uses_latest_quote_before_decision_time() {
fn next_minute_last_execution_uses_latest_quote_before_decision_time() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
@@ -5772,7 +5772,7 @@ mod tests {
&snapshot,
&[quote],
OrderSide::Sell,
MatchingType::NextTickLast,
MatchingType::NextMinuteLast,
Some(decision_time),
Some(decision_time),
200,
@@ -6026,7 +6026,7 @@ mod tests {
&snapshot,
&[quote],
OrderSide::Buy,
MatchingType::NextTickLast,
MatchingType::NextMinuteLast,
Some(start),
None,
100,
@@ -6103,7 +6103,7 @@ mod tests {
&snapshot,
&[quote],
OrderSide::Sell,
MatchingType::NextTickLast,
MatchingType::NextMinuteLast,
Some(start),
None,
100,
+1 -1
View File
@@ -3090,7 +3090,7 @@ fn normalize_history_frequency(frequency: &str) -> Option<String> {
let normalized = normalize_field(frequency);
match normalized.as_str() {
"1d" | "d" | "day" | "daily" => Some("1d".to_string()),
"1m" | "m" | "minute" | "min" | "tick" | "t" => Some("1m".to_string()),
"1m" | "m" | "minute" | "min" => Some("1m".to_string()),
_ => None,
}
}
+41 -41
View File
@@ -1444,7 +1444,7 @@ where
let snapshot = self.data.market(date, &intent.symbol);
if matches!(
self.broker.matching_type(),
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer
) {
let depth = self.data.order_book_depth_on(date, &intent.symbol);
if !depth.is_empty() {
@@ -1454,7 +1454,7 @@ where
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
for quote in quotes {
let price = match self.broker.matching_type() {
MatchingType::NextTickBestOwn => match intent.side() {
MatchingType::NextMinuteBestOwn => match intent.side() {
OrderSide::Buy => {
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
quote.bid1
@@ -1470,7 +1470,7 @@ where
}
}
},
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
match intent.side() {
OrderSide::Buy => quote.buy_price().unwrap_or(quote.last_price),
OrderSide::Sell => quote.sell_price().unwrap_or(quote.last_price),
@@ -2233,9 +2233,9 @@ where
"bar:post",
)?;
if should_run_tick_events(&schedule_rules, &self.subscriptions) {
if should_run_minute_events(&schedule_rules, &self.subscriptions) {
let filter_by_subscription = !self.subscriptions.is_empty();
let tick_quotes = self
let minute_quotes = self
.data
.execution_quotes_on_date(execution_date)
.into_iter()
@@ -2243,9 +2243,9 @@ where
!filter_by_subscription || self.subscriptions.contains(&quote.symbol)
})
.collect::<Vec<_>>();
for quote in tick_quotes {
let tick_time = quote.timestamp.time();
let tick_open_orders = self.open_order_views();
for quote in minute_quotes {
let minute_time = quote.timestamp.time();
let minute_open_orders = self.open_order_views();
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
@@ -2255,35 +2255,35 @@ where
&self.data,
&portfolio,
self.futures_account.as_ref(),
&tick_open_orders,
&minute_open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
&mut process_events,
execution_date,
ProcessEventKind::PreTick,
format!("tick:{}:{}:pre", quote.symbol, quote.timestamp),
ProcessEventKind::PreMinute,
format!("minute:{}:{}:pre", quote.symbol, quote.timestamp),
)?;
let mut tick_decision = collect_scheduled_decisions(
let mut minute_decision = collect_scheduled_decisions(
&mut self.strategy,
&scheduler,
execution_date,
ScheduleStage::Tick,
ScheduleStage::Minute,
&schedule_rules,
decision_date,
decision_index,
&self.data,
&portfolio,
self.futures_account.as_ref(),
&tick_open_orders,
&minute_open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
Some(tick_time),
Some(minute_time),
result.order_events.as_slice(),
result.fills.as_slice(),
)?;
tick_decision.merge_from(self.strategy.on_tick(
minute_decision.merge_from(self.strategy.on_minute(
&StrategyContext {
execution_date,
decision_date,
@@ -2291,7 +2291,7 @@ where
data: &self.data,
portfolio: &portfolio,
futures_account: self.futures_account.as_ref(),
open_orders: &tick_open_orders,
open_orders: &minute_open_orders,
dynamic_universe: self.dynamic_universe.as_ref(),
subscriptions: &self.subscriptions,
process_events: &process_events,
@@ -2311,42 +2311,42 @@ where
&self.data,
&portfolio,
self.futures_account.as_ref(),
&tick_open_orders,
&minute_open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
&mut process_events,
execution_date,
ProcessEventKind::Tick,
format!("tick:{}:{}", quote.symbol, quote.timestamp),
ProcessEventKind::Minute,
format!("minute:{}:{}", quote.symbol, quote.timestamp),
)?;
self.apply_strategy_directives(
execution_date,
decision_date,
decision_index,
&mut portfolio,
&tick_open_orders,
&minute_open_orders,
&mut process_events,
&mut tick_decision,
&mut minute_decision,
&mut directive_report,
)?;
let pre_tick_execution_orders = self.open_order_views();
let pre_minute_execution_orders = self.open_order_views();
self.ensure_execution_quotes_for_decision(
execution_date,
&portfolio,
&pre_tick_execution_orders,
&tick_decision,
Some(tick_time),
Some(tick_time),
&pre_minute_execution_orders,
&minute_decision,
Some(minute_time),
Some(minute_time),
)?;
let mut tick_report = self.broker.execute_between(
let mut minute_report = self.broker.execute_between(
execution_date,
&mut portfolio,
&self.data,
&tick_decision,
Some(tick_time),
Some(tick_time),
&minute_decision,
Some(minute_time),
Some(minute_time),
)?;
let post_tick_open_orders = self.open_order_views();
let post_minute_open_orders = self.open_order_views();
publish_process_events(
&mut self.strategy,
&mut self.process_event_bus,
@@ -2356,13 +2356,13 @@ where
&self.data,
&portfolio,
self.futures_account.as_ref(),
&post_tick_open_orders,
&post_minute_open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
&mut process_events,
&mut tick_report.process_events,
&mut minute_report.process_events,
)?;
merge_broker_report(&mut report, tick_report);
merge_broker_report(&mut report, minute_report);
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
@@ -2372,13 +2372,13 @@ where
&self.data,
&portfolio,
self.futures_account.as_ref(),
&post_tick_open_orders,
&post_minute_open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
&mut process_events,
execution_date,
ProcessEventKind::PostTick,
format!("tick:{}:{}:post", quote.symbol, quote.timestamp),
ProcessEventKind::PostMinute,
format!("minute:{}:{}:post", quote.symbol, quote.timestamp),
)?;
}
}
@@ -3709,7 +3709,7 @@ fn stage_label(stage: ScheduleStage) -> &'static str {
ScheduleStage::BeforeTrading => "before_trading",
ScheduleStage::OpenAuction => "open_auction",
ScheduleStage::Bar => "bar",
ScheduleStage::Tick => "tick",
ScheduleStage::Minute => "minute",
ScheduleStage::OnDay => "on_day",
ScheduleStage::AfterTrading => "after_trading",
ScheduleStage::Settlement => "settlement",
@@ -3723,8 +3723,8 @@ fn stage_datetime(
time.map(|value| date.and_time(value))
}
fn should_run_tick_events(rules: &[ScheduleRule], subscriptions: &BTreeSet<String>) -> bool {
!subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Tick)
fn should_run_minute_events(rules: &[ScheduleRule], subscriptions: &BTreeSet<String>) -> bool {
!subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Minute)
}
fn merge_broker_report(target: &mut BrokerExecutionReport, incoming: BrokerExecutionReport) {
+6 -6
View File
@@ -123,9 +123,9 @@ pub enum ProcessEventKind {
PreBar,
Bar,
PostBar,
PreTick,
Tick,
PostTick,
PreMinute,
Minute,
PostMinute,
PreScheduled,
PostScheduled,
PreOnDay,
@@ -165,9 +165,9 @@ impl ProcessEventKind {
Self::PreBar => "pre_bar",
Self::Bar => "bar",
Self::PostBar => "post_bar",
Self::PreTick => "pre_tick",
Self::Tick => "tick",
Self::PostTick => "post_tick",
Self::PreMinute => "pre_minute",
Self::Minute => "minute",
Self::PostMinute => "post_minute",
Self::PreScheduled => "pre_scheduled",
Self::PostScheduled => "post_scheduled",
Self::PreOnDay => "pre_on_day",
+13 -14
View File
@@ -286,7 +286,7 @@ fn band_low(index_close) {
stamp_tax_rate_after_change: None,
strict_value_budget: false,
slippage_model: SlippageModel::None,
matching_type: MatchingType::NextTickLast,
matching_type: MatchingType::NextMinuteLast,
quote_quantity_limit: true,
current_day_precomputed_factors: false,
prefer_precomputed_rolling_factors: false,
@@ -1343,7 +1343,7 @@ impl PlatformExprStrategy {
let last =
|| (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price);
match self.config.matching_type {
MatchingType::NextTickBestOwn => match side {
MatchingType::NextMinuteBestOwn => match side {
OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0)
.then_some(quote.bid1)
.or_else(last),
@@ -1351,18 +1351,17 @@ impl PlatformExprStrategy {
.then_some(quote.ask1)
.or_else(last),
},
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
match side {
OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(),
}
}
MatchingType::NextTickLast | MatchingType::Vwap | MatchingType::Twap => {
last().or_else(|| match side {
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => last()
.or_else(|| match side {
OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(),
})
}
}),
_ => match side {
OrderSide::Buy => quote.buy_price(),
OrderSide::Sell => quote.sell_price(),
@@ -8428,7 +8427,7 @@ mod tests {
}
#[test]
fn platform_aiquant_next_tick_last_projection_uses_last_quote_for_buy_budget() {
fn platform_aiquant_next_minute_last_projection_uses_last_quote_for_buy_budget() {
let date = d(2023, 5, 4);
let symbol = "000782.SZ";
let data = DataSet::from_components_with_actions_and_quotes(
@@ -8535,7 +8534,7 @@ mod tests {
cfg.minimum_commission = Some(5.0);
cfg.strict_value_budget = true;
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
cfg.matching_type = MatchingType::NextTickLast;
cfg.matching_type = MatchingType::NextMinuteLast;
cfg.quote_quantity_limit = false;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
let strategy = PlatformExprStrategy::new(cfg);
@@ -8668,7 +8667,7 @@ mod tests {
);
assert!(
18_600.0 * 6.5369 < target_value,
"fixture must be below target if tick price is used incorrectly"
"fixture must be below target if the minimum price unit is used incorrectly"
);
let ctx = StrategyContext {
@@ -8693,7 +8692,7 @@ mod tests {
cfg.minimum_commission = Some(5.0);
cfg.strict_value_budget = true;
cfg.slippage_model = SlippageModel::PriceRatio(0.002);
cfg.matching_type = MatchingType::NextTickLast;
cfg.matching_type = MatchingType::NextMinuteLast;
cfg.quote_quantity_limit = false;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(10, 40, 0).expect("time"));
let strategy = PlatformExprStrategy::new(cfg);
@@ -21321,7 +21320,7 @@ mod tests {
assert!(
strategy.stock_filter_uses_intraday_quote_fields(),
"actual tick/quote fields still require intraday selection quotes"
"actual minute quote fields still require intraday selection quotes"
);
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
@@ -21334,7 +21333,7 @@ mod tests {
);
assert!(
strategy.selection_uses_intraday_quote_fields(),
"selection ranking by tick fields must still request intraday selection quotes"
"selection ranking by minute fields must still request intraday selection quotes"
);
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
@@ -21490,7 +21489,7 @@ mod tests {
"000003.SZ".to_string(),
"000004.SZ".to_string(),
],
"daily close filter should run before tick quote prefetch; limit state remains a quote-time check"
"daily close filter should run before minute quote prefetch; limit state remains a quote-time check"
);
assert_eq!(
plan.order_symbols,
@@ -429,11 +429,9 @@ fn parse_matching_type(value: Option<&str>) -> Option<MatchingType> {
"open_auction" => Some(MatchingType::OpenAuction),
"current_bar_close" => Some(MatchingType::CurrentBarClose),
"next_bar_open" => Some(MatchingType::NextBarOpen),
"next_minute_last" | "next_tick_last" => Some(MatchingType::NextTickLast),
"next_minute_best_own" | "next_tick_best_own" => Some(MatchingType::NextTickBestOwn),
"next_minute_best_counterparty" | "next_tick_best_counterparty" => {
Some(MatchingType::NextTickBestCounterparty)
}
"next_minute_last" => Some(MatchingType::NextMinuteLast),
"next_minute_best_own" => Some(MatchingType::NextMinuteBestOwn),
"next_minute_best_counterparty" => Some(MatchingType::NextMinuteBestCounterparty),
"counterparty_offer" => Some(MatchingType::CounterpartyOffer),
"vwap" => Some(MatchingType::Vwap),
"twap" => Some(MatchingType::Twap),
@@ -1695,7 +1693,7 @@ mod tests {
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!(cfg.matching_type, MatchingType::NextTickLast);
assert_eq!(cfg.matching_type, MatchingType::NextMinuteLast);
assert_eq!(cfg.slippage_model, SlippageModel::PriceRatio(0.001));
assert!(cfg.strict_value_budget);
}
+1 -1
View File
@@ -112,7 +112,7 @@ impl ChinaAShareRiskControl {
return Some("paused");
}
// `allow_sell` is derived from the daily candidate snapshot and may
// reflect an open/close fallback rather than the actual execution tick.
// reflect an open/close fallback rather than the actual execution price.
// A sell order must be blocked by the execution price lower-limit check
// below, while suspension and delisting are handled above.
if market.is_at_lower_limit_price(check_price) {
+2 -2
View File
@@ -7,7 +7,7 @@ pub enum ScheduleStage {
BeforeTrading,
OpenAuction,
Bar,
Tick,
Minute,
OnDay,
AfterTrading,
Settlement,
@@ -225,7 +225,7 @@ pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
ScheduleStage::Tick => None,
ScheduleStage::Minute => None,
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
+1 -1
View File
@@ -68,7 +68,7 @@ pub trait Strategy {
fn on_bar(&mut self, _ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> {
Ok(StrategyDecision::default())
}
fn on_tick(
fn on_minute(
&mut self,
_ctx: &StrategyContext<'_>,
_quote: &IntradayExecutionQuote,
+9 -9
View File
@@ -97,7 +97,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
"平台策略脚本采用声明式 DSL + 表达式执行模型。".to_string(),
"支持 let 变量、fn 自定义函数、when/unless/else 条件块、可用指标/因子字段映射。".to_string(),
"支持数值型和字符串型因子,字符串字段可用于行业、概念、标签、板块等分类过滤。".to_string(),
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、tick 触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
"当前默认回测数据已支持 OHLCV、市值、流通市值、换手率、有效换手率、上市天数、停牌/ST/板块、涨跌停价格、分钟线触达涨跌停、常用价格/成交量均线,以及 stock_indicator_factors_v1 中已入库的通用指标因子。".to_string(),
"AI 生成策略时只能输出完整 engine-script 代码,不输出 Markdown、解释、推理过程、JSON 包装或手册复述。".to_string(),
"表达式字段以运行时字段为准:市值使用 market_cap,流通市值使用 free_float_cap;不要在策略表达式中使用数据库原始字段 float_market_cap。".to_string(),
"任意窗口价格均线使用 rolling_mean(\"close\", n) 或 ma(\"close\", n),任意窗口均量使用 rolling_mean(\"volume\", n) 或 vma(n);不要使用未列出的 ma60、stock_ma60、signal_ma60 或 benchmark_ma60 变量。".to_string(),
@@ -204,7 +204,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualSection {
title: "bar / minute execution 生命周期".to_string(),
detail: "回测内核支持 平台内核 风格的 bar/分钟执行价生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在分钟执行价订阅或分钟调度规则时,会按 execution_quotes 的时间顺序发布 pre_tick/tick/post_tick 兼容事件,并把日内阶段下单限制在当前分钟执行价时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟日内订阅策略。".to_string(),
detail: "回测内核支持 平台内核 风格的 bar/分钟执行价生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在分钟执行价订阅或分钟调度规则时,会按 execution_quotes 的时间顺序发布 pre_minute/minute/post_minute 过程事件,并把日内阶段下单限制在当前分钟执行价时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟日内订阅策略。".to_string(),
},
ManualSection {
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
@@ -220,11 +220,11 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualSection {
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_tick_* 仅作为兼容别名,内部仍读取分钟执行价。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(\"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(),
},
ManualSection {
title: "期货提交校验".to_string(),
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、limit price tick、price limit 校验,用于兼容特殊数据,但默认全部开启。".to_string(),
detail: "期货订单进入撮合前会先执行账户与交易规则校验:合约必须在上市/退市日期范围内,日行情不能停牌,trading_phase 需处于 continuous/trading/open_auction/auction/call_auction/opening_auction 等可交易阶段,限价必须为正且按 futures_trading_parameters.price_tick 或日行情 price_tick 对齐,并且不能越过 upper_limit/lower_limit;随后继续检查反向挂单自成交风险、保证金和可平数量。服务层可通过 FuturesValidationConfig 分别关闭 active instrument、trading phase、限价最小价位、price limit 校验,但默认全部开启。".to_string(),
},
ManualSection {
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
@@ -267,7 +267,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
fields: vec![
ManualField { name: "symbol".to_string(), field_type: "string".to_string(), detail: "证券代码。".to_string() },
ManualField { name: "market_cap/free_float_cap".to_string(), field_type: "float".to_string(), detail: "总市值、流通市值。".to_string() },
ManualField { name: "turnover/turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率、换手率标准字段有效换手率turnover 是 turnover_ratio 的兼容别名".to_string() },
ManualField { name: "turnover_ratio/effective_turnover_ratio".to_string(), field_type: "float".to_string(), detail: "换手率标准字段有效换手率。".to_string() },
ManualField { name: "open/high/low/close/last/last_price/prev_close/amount".to_string(), field_type: "float".to_string(), detail: "开盘、最高、最低、收盘、盘中价、昨收和成交额。".to_string() },
ManualField { name: "upper_limit/lower_limit/price_tick/round_lot/minimum_order_quantity/order_step_size".to_string(), field_type: "float/int".to_string(), detail: "涨跌停、最小价位、整手、最小下单量和数量步长。KSH/BJSE 等板块可与 round_lot 不同。".to_string() },
ManualField { name: "paused/is_st/is_kcb/is_one_yuan/is_new_listing".to_string(), field_type: "bool".to_string(), detail: "可交易性与板块标志。".to_string() },
@@ -304,13 +304,13 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
functions: vec![
ManualFunction { name: "factor".to_string(), signature: "factor(\"column_name\")".to_string(), detail: "读取当前股票当日可用因子列。数值因子返回 float,字符串因子返回 string;缺失字段默认返回 0 或空字符串,建议重要条件配合 diagnostics 查看候选过滤数量。".to_string() },
ManualFunction { name: "day_factor".to_string(), signature: "day_factor(\"field_name\")".to_string(), detail: "读取日级/指数级字段映射。".to_string() },
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟字段支持 last/bid1/ask1/volume_delta/amount_delta。\"tick\" 频率只作为兼容别名映射到 \"1m\",不再表示逐笔数据。日线 include_now=false 排除当前交易日;分钟线会按当前 on_bar、日内事件或调度时刻截断,include_now=false 排除当前分钟执行价,避免未来函数。".to_string() },
ManualFunction { name: "history_bars".to_string(), signature: "ctx.history_bars(symbol, count, \"1d\" | \"1m\", \"close\", include_now)".to_string(), detail: "回测内核策略上下文数据 API,返回指定证券最近 N 条数值序列。日线字段支持 open/high/low/close/last/prev_close/volume/upper_limit/lower_limit;分钟字段支持 last/bid1/ask1/volume_delta/amount_delta。日线 include_now=false 排除当前交易日;分钟线会按当前 on_bar、日内事件或调度时刻截断,include_now=false 排除当前分钟执行价,避免未来函数。".to_string() },
ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前交易日指定证券的日级快照,可用于获得当日 open/close/last/upper_limit/lower_limit 等字段。".to_string() },
ManualFunction { name: "instrument/instruments/all_instruments".to_string(), signature: "ctx.instrument(symbol)".to_string(), detail: "读取证券元数据,包括名称、板块、上市日期、退市日期、最小下单量、整手、最小价位等;all_instruments 按证券代码稳定排序返回全量证券。".to_string() },
ManualFunction { name: "active_instruments/instruments_history".to_string(), signature: "ctx.active_instruments(&[symbol])".to_string(), detail: "active_instruments 返回当前交易日已上市且未退市的证券;instruments_history 返回给定代码的历史证券记录,包含当前已退市标的,对齐 平台内核 的 active_instruments/instruments_history 能力。".to_string() },
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。".to_string() },
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟线返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
@@ -365,7 +365,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualFactorSource {
table: "期货交易参数".to_string(),
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价 tick 校验。".to_string(),
detail: "字段包括 symbol、effective_date、contract_multiplier、long_margin_rate、short_margin_rate、commission_type、open_commission_ratio、close_commission_ratio、close_today_commission_ratio、price_tick。回测会按交易日自动选择不晚于当前日期的最新参数,用于保证金、手续费和限价最小价位校验。".to_string(),
fields: vec![],
},
],
@@ -518,7 +518,7 @@ pub fn build_generation_prompt(
prompt.push_str("- 生成的代码必须能转换为 strategy_spec 并提交 POST /v1/backtests。\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("参数形态必须严格: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_auction旧 next_tick_* 只是兼容别名,新生成策略不要使用;next_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 只能取 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("回测成功但 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("用户目标:\n");
@@ -190,7 +190,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickLast)
.with_matching_type(MatchingType::NextMinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
initial_cash: 10_000.0,
@@ -385,7 +385,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickLast)
.with_matching_type(MatchingType::NextMinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
initial_cash: 10_000.0,
@@ -587,7 +587,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickLast)
.with_matching_type(MatchingType::NextMinuteLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
initial_cash: 10_000.0,
+16 -16
View File
@@ -799,7 +799,7 @@ impl Strategy for UniverseDirectiveStrategy {
impl Strategy for TickProbeStrategy {
fn name(&self) -> &str {
"tick-probe"
"minute-probe"
}
fn on_day(
@@ -812,26 +812,26 @@ impl Strategy for TickProbeStrategy {
exit_symbols: BTreeSet::new(),
order_intents: vec![OrderIntent::Subscribe {
symbols: BTreeSet::from(["000001.SZ".to_string()]),
reason: "subscribe_tick_probe".to_string(),
reason: "subscribe_minute_probe".to_string(),
}],
notes: Vec::new(),
diagnostics: Vec::new(),
})
}
fn on_tick(
fn on_minute(
&mut self,
ctx: &StrategyContext<'_>,
quote: &IntradayExecutionQuote,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
let visible_last = ctx
.history_bars(&quote.symbol, 9, "tick", "last", true)
.history_bars(&quote.symbol, 9, "1m", "last", true)
.iter()
.map(|value| format!("{value:.2}"))
.collect::<Vec<_>>()
.join(",");
let previous_last = ctx
.history_bars(&quote.symbol, 9, "tick", "last", false)
.history_bars(&quote.symbol, 9, "1m", "last", false)
.iter()
.map(|value| format!("{value:.2}"))
.collect::<Vec<_>>()
@@ -853,7 +853,7 @@ impl Strategy for TickProbeStrategy {
order_intents: vec![OrderIntent::Shares {
symbol: quote.symbol.clone(),
quantity: 100,
reason: "tick_buy".to_string(),
reason: "minute_buy".to_string(),
}],
notes: Vec::new(),
diagnostics: Vec::new(),
@@ -919,10 +919,10 @@ impl Strategy for DataApiProbeStrategy {
let daily_price_count = ctx
.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "1d")
.len();
let tick_alias_price_bars =
ctx.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "tick");
let tick_price_count = tick_alias_price_bars.len();
let tick_alias_frequency = tick_alias_price_bars
let minute_price_bars =
ctx.get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "1m");
let minute_price_count = minute_price_bars.len();
let minute_frequency = minute_price_bars
.first()
.map(|bar| bar.frequency.clone())
.unwrap_or_default();
@@ -930,7 +930,7 @@ impl Strategy for DataApiProbeStrategy {
ctx.instruments_history(&["000001.SZ", "000002.SZ"]).len();
let active_instrument_count = ctx.active_instruments(&["000001.SZ", "000002.SZ"]).len();
self.snapshots.borrow_mut().push(format!(
"daily={daily_close};previous={previous_close};minute={tick_last};previous_minute={previous_tick_last};current={current_close};instrument={instrument_name};all={};history={instrument_history_count};active={active_instrument_count};range={trading_date_count};prev={prev_date};next={next_date};suspended={suspended};st={st_flags};price_daily={daily_price_count};price_tick={tick_price_count};tick_alias_frequency={tick_alias_frequency}",
"daily={daily_close};previous={previous_close};minute={tick_last};previous_minute={previous_tick_last};current={current_close};instrument={instrument_name};all={};history={instrument_history_count};active={active_instrument_count};range={trading_date_count};prev={prev_date};next={next_date};suspended={suspended};st={st_flags};price_daily={daily_price_count};price_tick={minute_price_count};minute_frequency={minute_frequency}",
ctx.all_instruments().len()
));
}
@@ -2046,25 +2046,25 @@ fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() {
]
);
assert_eq!(result.fills.len(), 1);
assert_eq!(result.fills[0].reason, "tick_buy");
assert_eq!(result.fills[0].reason, "minute_buy");
assert_eq!(result.fills[0].quantity, 100);
assert!(
result
.process_events
.iter()
.any(|event| event.kind == ProcessEventKind::PreTick)
.any(|event| event.kind == ProcessEventKind::PreMinute)
);
assert!(
result
.process_events
.iter()
.any(|event| event.kind == ProcessEventKind::Tick)
.any(|event| event.kind == ProcessEventKind::Minute)
);
assert!(
result
.process_events
.iter()
.any(|event| event.kind == ProcessEventKind::PostTick)
.any(|event| event.kind == ProcessEventKind::PostMinute)
);
}
@@ -2253,7 +2253,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
assert_eq!(
snapshots.borrow().as_slice(),
[
"daily=10.10,10.20;previous=10.00,10.10;minute=10.15,10.25;previous_minute=10.15;current=10.20;instrument=Anchor;all=2;history=2;active=1;range=3;prev=2025-01-03;next=2025-01-06;suspended=0,1,0;st=0,1,0;price_daily=2;price_tick=3;tick_alias_frequency=1m"
"daily=10.10,10.20;previous=10.00,10.10;minute=10.15,10.25;previous_minute=10.15;current=10.20;instrument=Anchor;all=2;history=2;active=1;range=3;prev=2025-01-03;next=2025-01-06;suspended=0,1,0;st=0,1,0;price_daily=2;price_tick=3;minute_frequency=1m"
]
);
}
@@ -349,7 +349,7 @@ fn broker_delayed_limit_open_sell_uses_tick_price() {
ChinaEquityRuleHooks::default(),
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickLast)
.with_matching_type(MatchingType::NextMinuteLast)
.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 31, 0).unwrap())
.with_volume_limit(false)
.with_liquidity_limit(false);
@@ -1809,7 +1809,7 @@ fn broker_rejects_intraday_last_order_without_execution_quotes() {
order_intents: vec![OrderIntent::Value {
symbol: "000002.SZ".to_string(),
value: 100_000.0,
reason: "missing_tick_quotes".to_string(),
reason: "missing_minute_quotes".to_string(),
}],
notes: Vec::new(),
diagnostics: Vec::new(),
@@ -2173,7 +2173,7 @@ fn broker_cancels_market_buy_when_tick_has_no_volume() {
report.order_events[0].status,
fidc_core::OrderStatus::Canceled
);
assert!(report.order_events[0].reason.contains("tick no volume"));
assert!(report.order_events[0].reason.contains("minute no volume"));
}
#[test]
@@ -2888,7 +2888,7 @@ fn broker_uses_best_own_price_for_intraday_matching() {
ChinaEquityRuleHooks::default(),
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickBestOwn);
.with_matching_type(MatchingType::NextMinuteBestOwn);
let report = broker
.execute(
@@ -3003,7 +3003,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() {
ChinaEquityRuleHooks::default(),
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickBestCounterparty);
.with_matching_type(MatchingType::NextMinuteBestCounterparty);
let report = broker
.execute(