From f7d0889bbc593e68b8dc549faa0333a18524b30a Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 10 Jul 2026 04:21:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=9B=AE=E6=A0=87=E7=BB=84?= =?UTF-8?q?=E5=90=88=E6=89=A7=E8=A1=8C=E6=97=A5=E5=B1=95=E5=BC=80=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/engine.rs | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 9ae39b8..88ac7cd 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -4286,6 +4286,43 @@ mod tests { } } + #[derive(Debug)] + struct ScheduledTargetPortfolioSmartStrategy { + rule: ScheduleRule, + decision_date: NaiveDate, + target_weights: BTreeMap, + } + + impl Strategy for ScheduledTargetPortfolioSmartStrategy { + fn name(&self) -> &str { + "scheduled_target_portfolio_smart" + } + + fn schedule_rules(&self) -> Vec { + vec![self.rule.clone()] + } + + fn on_scheduled( + &mut self, + ctx: &StrategyContext<'_>, + rule: &ScheduleRule, + ) -> Result { + assert_eq!(rule.name, self.rule.name); + if ctx.decision_date != self.decision_date { + return Ok(StrategyDecision::default()); + } + Ok(StrategyDecision { + order_intents: vec![OrderIntent::TargetPortfolioSmart { + target_weights: self.target_weights.clone(), + order_prices: None, + valuation_prices: None, + reason: "scheduled_target_portfolio_smart".to_string(), + }], + ..StrategyDecision::default() + }) + } + } + #[derive(Debug)] struct ScheduledEligibleUniverseBuyStrategy { rule: ScheduleRule, @@ -4921,6 +4958,51 @@ mod tests { assert_eq!(result.fills[0].price, 12.0); } + #[test] + fn next_bar_open_target_portfolio_smart_sizes_with_execution_day_open() { + let first = d(2025, 1, 2); + let second = d(2025, 1, 3); + let dataset = DataSet::from_components( + vec![default_instrument()], + vec![market(first, 10.0, 10.0), market(second, 12.0, 12.0)], + vec![factor(first), factor(second)], + vec![candidate(first), candidate(second)], + vec![benchmark(first), benchmark(second)], + ) + .expect("dataset"); + let broker = scheduled_next_open_broker(FidcRiskControlConfig::default()); + let config = BacktestConfig { + initial_cash: 100_000.0, + benchmark_code: "000852.SH".to_string(), + start_date: Some(first), + end_date: Some(second), + decision_lag_trading_days: 1, + execution_price_field: PriceField::Open, + }; + let mut target_weights = BTreeMap::new(); + target_weights.insert(SYMBOL.to_string(), 1.0); + + let result = BacktestEngine::new( + dataset, + ScheduledTargetPortfolioSmartStrategy { + rule: ScheduleRule::daily("daily_target_portfolio", ScheduleStage::OnDay), + decision_date: first, + target_weights, + }, + broker, + config, + ) + .run() + .expect("backtest run"); + + assert_eq!(result.fills.len(), 1); + assert_eq!(result.fills[0].date, second); + assert_eq!(result.fills[0].decision_date, Some(first)); + assert_eq!(result.fills[0].execution_date, Some(second)); + assert_eq!(result.fills[0].price, 12.0); + assert_eq!(result.fills[0].quantity, 8_300); + } + #[test] fn next_bar_open_executes_last_decision_without_execution_day_factor_snapshot() { let first = d(2025, 1, 2);