From 7f40cfdab0f4978e81cf178e3e62f3fbef56f472 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 26 Jun 2026 13:27:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=9B=9E=E6=B5=8B=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E4=BB=B7=E4=B8=BA=E5=88=86=E9=92=9F=E7=BA=BF=E8=AF=AD?= =?UTF-8?q?=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- crates/fidc-core/src/broker.rs | 52 ++++++------ crates/fidc-core/src/data.rs | 2 +- crates/fidc-core/src/engine.rs | 82 +++++++++---------- crates/fidc-core/src/events.rs | 12 +-- .../fidc-core/src/platform_expr_strategy.rs | 27 +++--- .../fidc-core/src/platform_strategy_spec.rs | 10 +-- crates/fidc-core/src/risk_control.rs | 2 +- crates/fidc-core/src/scheduler.rs | 4 +- crates/fidc-core/src/strategy.rs | 2 +- crates/fidc-core/src/strategy_ai.rs | 18 ++-- .../fidc-core/tests/decision_quote_preload.rs | 6 +- crates/fidc-core/tests/engine_hooks.rs | 32 ++++---- crates/fidc-core/tests/explicit_order_flow.rs | 10 +-- 14 files changed, 129 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 209ce0e..4c59305 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 当前能力 -- 日频和分钟执行价策略生命周期与确定性回放;旧 tick 入口仅作为兼容别名映射到分钟执行价。 +- 日频和分钟执行价策略生命周期与确定性回放。 - A 股行情、估值、因子、基准、候选资格、涨跌停触达、停牌和 ST 标记。 - 平台策略 DSL 与 `StrategyContext` 数据 API,不暴露非平台脚本语法。 - `BacktestConfig` 支持起止日期、初始资金、决策滞后、执行价格字段、基准代码。 diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 1e3c41f..ffe4faa 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -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 { 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, 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, diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index b20b954..ce1cf2d 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -3090,7 +3090,7 @@ fn normalize_history_frequency(frequency: &str) -> Option { 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, } } diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 2630605..cbbca39 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -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("e.symbol) }) .collect::>(); - 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) -> bool { - !subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Tick) +fn should_run_minute_events(rules: &[ScheduleRule], subscriptions: &BTreeSet) -> bool { + !subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Minute) } fn merge_broker_report(target: &mut BrokerExecutionReport, incoming: BrokerExecutionReport) { diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index 02aca7a..0499b7c 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -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", diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 71ad190..3d23556 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -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, diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index 2ff8319..cc4f2a5 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -429,11 +429,9 @@ fn parse_matching_type(value: Option<&str>) -> Option { "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); } diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 6b8ffbf..6b11f9f 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -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) { diff --git a/crates/fidc-core/src/scheduler.rs b/crates/fidc-core/src/scheduler.rs index 9077cbf..a260517 100644 --- a/crates/fidc-core/src/scheduler.rs +++ b/crates/fidc-core/src/scheduler.rs @@ -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 { 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")), diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index a1507e3..4635269 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -68,7 +68,7 @@ pub trait Strategy { fn on_bar(&mut self, _ctx: &StrategyContext<'_>) -> Result { Ok(StrategyDecision::default()) } - fn on_tick( + fn on_minute( &mut self, _ctx: &StrategyContext<'_>, _quote: &IntradayExecutionQuote, diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index 80bbf08..75c5f7a 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -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_price;next_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_price;next_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_ma60,60日价格均线写 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 == 0;risk.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_ma60,60日价格均线写 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 == 0;risk.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_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"); diff --git a/crates/fidc-core/tests/decision_quote_preload.rs b/crates/fidc-core/tests/decision_quote_preload.rs index e44af2d..6f6bca7 100644 --- a/crates/fidc-core/tests/decision_quote_preload.rs +++ b/crates/fidc-core/tests/decision_quote_preload.rs @@ -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, diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 04014a3..dbd64e1 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -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 { let visible_last = ctx - .history_bars("e.symbol, 9, "tick", "last", true) + .history_bars("e.symbol, 9, "1m", "last", true) .iter() .map(|value| format!("{value:.2}")) .collect::>() .join(","); let previous_last = ctx - .history_bars("e.symbol, 9, "tick", "last", false) + .history_bars("e.symbol, 9, "1m", "last", false) .iter() .map(|value| format!("{value:.2}")) .collect::>() @@ -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" ] ); } diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index 7bbdd94..7db9437 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -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(