diff --git a/crates/fidc-core/src/lib.rs b/crates/fidc-core/src/lib.rs index a77b2a7..26dca7e 100644 --- a/crates/fidc-core/src/lib.rs +++ b/crates/fidc-core/src/lib.rs @@ -35,9 +35,9 @@ pub use events::{ pub use instrument::Instrument; pub use metrics::{BacktestMetrics, compute_backtest_metrics}; pub use platform_expr_strategy::{ - PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy, - PlatformExprStrategyConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency, - PlatformTradeAction, + PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind, + PlatformExprStrategy, PlatformExprStrategyConfig, PlatformRebalanceSchedule, + PlatformScheduleFrequency, PlatformTradeAction, }; pub use portfolio::{CashReceivable, HoldingSummary, PortfolioState, Position}; pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck}; diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 860c765..d1f317f 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -115,6 +115,12 @@ pub enum PlatformTradeAction { }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlatformExplicitActionStage { + OpenAuction, + OnDay, +} + #[derive(Debug, Clone)] pub struct PlatformExprStrategyConfig { pub strategy_name: String, @@ -145,6 +151,7 @@ pub struct PlatformExprStrategyConfig { pub skip_month_day_ranges: Vec<(u32, u32, u32)>, pub rebalance_schedule: Option, pub rotation_enabled: bool, + pub explicit_action_stage: PlatformExplicitActionStage, pub explicit_actions: Vec, } @@ -192,6 +199,7 @@ fn band_low(index_close) { skip_month_day_ranges: Vec::new(), rebalance_schedule: None, rotation_enabled: true, + explicit_action_stage: PlatformExplicitActionStage::OnDay, explicit_actions: Vec::new(), } } @@ -2231,6 +2239,32 @@ impl PlatformExprStrategy { Ok(intents) } + fn explicit_action_decision( + &self, + ctx: &StrategyContext<'_>, + ) -> Result { + let day = self.day_state(ctx, ctx.execution_date)?; + let order_intents = self.explicit_action_intents(ctx, ctx.execution_date, &day)?; + let diagnostics = vec![format!( + "platform_expr signal={} last={:.2} explicit_actions={} stage={}", + self.config.signal_symbol, + day.signal_close, + self.config.explicit_actions.len(), + match self.config.explicit_action_stage { + PlatformExplicitActionStage::OpenAuction => "open_auction", + PlatformExplicitActionStage::OnDay => "on_day", + } + )]; + Ok(StrategyDecision { + rebalance: false, + target_weights: BTreeMap::new(), + exit_symbols: BTreeSet::new(), + order_intents, + notes: Vec::new(), + diagnostics, + }) + } + fn stock_passes_expr( &self, ctx: &StrategyContext<'_>, @@ -2546,6 +2580,18 @@ impl Strategy for PlatformExprStrategy { self.config.strategy_name.as_str() } + fn open_auction( + &mut self, + ctx: &StrategyContext<'_>, + ) -> Result { + if self.config.explicit_action_stage == PlatformExplicitActionStage::OpenAuction + && !self.config.explicit_actions.is_empty() + { + return self.explicit_action_decision(ctx); + } + Ok(StrategyDecision::default()) + } + fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result { let date = ctx.execution_date; if self.config.in_skip_window(date) { @@ -2570,7 +2616,12 @@ impl Strategy for PlatformExprStrategy { } let day = self.day_state(ctx, date)?; - let explicit_action_intents = self.explicit_action_intents(ctx, date, &day)?; + let explicit_action_intents = + if self.config.explicit_action_stage == PlatformExplicitActionStage::OnDay { + self.explicit_action_intents(ctx, date, &day)? + } else { + Vec::new() + }; let mut selection_notes = Vec::new(); let trading_ratio = if self.config.rotation_enabled { self.trading_ratio(ctx, &day)? @@ -2812,9 +2863,9 @@ mod tests { use chrono::NaiveDate; use super::{ - PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy, - PlatformExprStrategyConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency, - PlatformTradeAction, + PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind, + PlatformExprStrategy, PlatformExprStrategyConfig, PlatformRebalanceSchedule, + PlatformScheduleFrequency, PlatformTradeAction, }; use crate::{ BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, @@ -2986,4 +3037,108 @@ mod tests { .any(|item| item.contains("rotation=false")) ); } + + #[test] + fn platform_strategy_emits_explicit_actions_in_open_auction_stage() { + let date = d(2025, 2, 3); + let data = DataSet::from_components( + vec![Instrument { + symbol: "000001.SZ".to_string(), + name: "Ping An Bank".to_string(), + board: "SZSE".to_string(), + round_lot: 100, + listed_at: Some(d(2010, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: "000001.SZ".to_string(), + timestamp: Some("09:25:00".to_string()), + day_open: 10.0, + open: 10.0, + high: 10.2, + low: 9.9, + close: 10.1, + last_price: 10.0, + bid1: 9.99, + ask1: 10.0, + prev_close: 9.95, + volume: 1_000_000, + tick_volume: 5_000, + bid1_volume: 1_000, + ask1_volume: 1_000, + trading_phase: Some("open_auction".to_string()), + paused: false, + upper_limit: 10.94, + lower_limit: 8.96, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: "000001.SZ".to_string(), + market_cap_bn: 12.0, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(22.0), + effective_turnover_ratio: Some(18.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: "000001.SZ".to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1002.0, + prev_close: 998.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let portfolio = PortfolioState::new(1_000_000.0); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 0, + data: &data, + portfolio: &portfolio, + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.signal_symbol = "000001.SZ".to_string(); + cfg.rotation_enabled = false; + cfg.explicit_action_stage = PlatformExplicitActionStage::OpenAuction; + cfg.benchmark_short_ma_days = 1; + cfg.benchmark_long_ma_days = 1; + cfg.explicit_actions = vec![PlatformTradeAction::Order { + kind: PlatformExplicitOrderKind::Percent, + symbol: "000001.SZ".to_string(), + amount_expr: "0.25".to_string(), + limit_price_expr: None, + when_expr: Some("allow_buy".to_string()), + reason: "auction_percent_entry".to_string(), + }]; + let mut strategy = PlatformExprStrategy::new(cfg); + + let auction_decision = strategy.open_auction(&ctx).expect("auction decision"); + assert_eq!(auction_decision.order_intents.len(), 1); + assert!( + auction_decision + .diagnostics + .iter() + .any(|item| item.contains("stage=open_auction")) + ); + + let on_day_decision = strategy.on_day(&ctx).expect("on day decision"); + assert!(on_day_decision.order_intents.is_empty()); + } }