diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 4ba0b48..d0edc75 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -2426,18 +2426,26 @@ where } else { ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field) }; - let mut risk_config = self.risk_config.clone(); - if self.aiquant_execution_rules { - risk_config.static_rules.respect_allow_buy_sell = false; - } + let adjusted_candidate; + let candidate_for_check = if self.aiquant_execution_rules + && self.aiquant_sell_allow_flag_is_stale_lower_limit(candidate, snapshot, check_price) + { + adjusted_candidate = crate::data::CandidateEligibility { + allow_sell: true, + ..candidate.clone() + }; + &adjusted_candidate + } else { + candidate + }; if let Some(reason) = ChinaAShareRiskControl::sell_rejection_reason_with_config( date, - candidate, + candidate_for_check, snapshot, instrument, Some(position), check_price, - &risk_config, + &self.risk_config, ) { return RuleCheck::reject(reason); } @@ -2453,6 +2461,21 @@ where RuleCheck::allow() } + fn aiquant_sell_allow_flag_is_stale_lower_limit( + &self, + candidate: &crate::data::CandidateEligibility, + snapshot: &crate::data::DailyMarketSnapshot, + check_price: f64, + ) -> bool { + if candidate.allow_sell || candidate.is_paused || snapshot.paused { + return false; + } + let snapshot_check_price = + ChinaAShareRiskControl::sell_check_price(snapshot, self.execution_price_field); + snapshot.is_at_lower_limit_price(snapshot_check_price) + && !snapshot.is_at_lower_limit_price(check_price) + } + fn minimum_target_quantity( &self, date: NaiveDate, @@ -5283,7 +5306,7 @@ where } if self.volume_limit { - let raw_limit = ((available_market_volume as f64) * self.volume_percent).round() as i64 + let raw_limit = ((available_market_volume as f64) * self.volume_percent).floor() as i64 - consumed_turnover as i64; if raw_limit <= 0 { return Err(volume_limit_reason.to_string()); @@ -6537,6 +6560,27 @@ mod tests { assert_eq!(fillable, Ok(5_000)); } + #[test] + fn volume_limit_uses_floor_for_odd_lot_sell() { + let mut snapshot = limit_test_snapshot(); + snapshot.minute_volume = 0; + snapshot.volume = 3; + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks, + PriceField::Close, + ) + .with_matching_type(MatchingType::CurrentBarClose) + .with_volume_limit(true) + .with_volume_percent(0.5) + .with_liquidity_limit(false); + + let fillable = + broker.market_fillable_quantity(&snapshot, OrderSide::Sell, 10, 100, 100, 0, true); + + assert_eq!(fillable, Ok(1)); + } + #[test] fn current_bar_close_volume_limit_rejects_daily_zero_volume() { let mut snapshot = limit_test_snapshot(); @@ -7427,4 +7471,99 @@ mod tests { report.order_events ); } + + #[test] + fn aiquant_sell_rule_still_blocks_real_allow_sell_false() { + let date = chrono::NaiveDate::from_ymd_opt(2024, 4, 17).expect("valid date"); + let prev_date = chrono::NaiveDate::from_ymd_opt(2024, 4, 16).expect("valid date"); + let symbol = "000001.SZ"; + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks, + PriceField::Last, + ) + .with_aiquant_execution_rules(true) + .with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time()) + .with_volume_limit(false) + .with_liquidity_limit(false) + .with_inactive_limit(false) + .with_slippage_model(SlippageModel::None); + + let mut snapshot = limit_test_snapshot(); + snapshot.date = date; + snapshot.timestamp = Some("2024-04-17 10:18:00".to_string()); + snapshot.day_open = 10.0; + snapshot.open = 10.0; + snapshot.high = 10.2; + snapshot.low = 9.8; + snapshot.close = 10.0; + snapshot.last_price = 10.0; + snapshot.bid1 = 10.0; + snapshot.ask1 = 10.0; + snapshot.prev_close = 10.0; + snapshot.upper_limit = 11.0; + snapshot.lower_limit = 9.0; + let mut candidate = limit_test_candidate(true, false); + candidate.date = date; + let mut quote = limit_test_quote(10.0, 9.99, 10.0); + quote.date = date; + quote.timestamp = date.and_hms_opt(10, 18, 0).unwrap(); + quote.last_price = 10.0; + quote.bid1 = 9.99; + quote.ask1 = 10.0; + + let data = DataSet::from_components_with_actions_and_quotes( + vec![limit_test_instrument()], + vec![snapshot], + Vec::new(), + vec![candidate], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1000.0, + prev_close: 1000.0, + volume: 1_000_000, + }], + Vec::new(), + vec![quote], + ) + .expect("valid dataset"); + let mut portfolio = PortfolioState::new(100.0); + portfolio.position_mut(symbol).buy(prev_date, 7_200, 8.48); + let mut report = BrokerExecutionReport::default(); + + broker + .process_target_value( + date, + &mut portfolio, + &data, + symbol, + 0.0, + "stop_loss_exit", + &mut BTreeMap::new(), + &mut BTreeMap::new(), + &mut None, + &mut BTreeMap::new(), + &mut report, + ) + .expect("target sell processed"); + + assert!(report.fill_events.is_empty()); + assert_eq!( + portfolio.position(symbol).map(|position| position.quantity), + Some(7_200) + ); + let rejected_order = report + .order_events + .iter() + .find(|event| event.side == OrderSide::Sell) + .expect("sell order event"); + assert_eq!(rejected_order.status, OrderStatus::Canceled); + assert!( + rejected_order.reason.contains("sell_disabled"), + "{}", + rejected_order.reason + ); + } } diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 9a0015d..0c1b2ee 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -3516,8 +3516,13 @@ mod tests { ) .expect("dataset"); - assert!(data.eligible_universe_on(date).is_empty()); + assert_eq!(data.eligible_universe_on(date).len(), 1); let mut risk_config = FidcRiskControlConfig::default(); + risk_config.static_rules.reject_kcb_selection = true; + assert!( + data.eligible_universe_on_with_risk_config(date, &risk_config) + .is_empty() + ); risk_config.static_rules.reject_kcb_selection = false; let rows = data.eligible_universe_on_with_risk_config(date, &risk_config); diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index f3efec1..9ae39b8 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -5002,9 +5002,22 @@ mod tests { .map(|snapshot| snapshot.close), Some(11.5) ); + assert_eq!( + dataset + .eligible_universe_on(first) + .iter() + .map(|row| row.symbol.clone()) + .collect::>(), + vec![SYMBOL.to_string()], + "raw DataSet helper should not apply default selection risk" + ); + let mut selection_risk_config = FidcRiskControlConfig::default(); + selection_risk_config.static_rules.reject_paused_selection = true; assert!( - dataset.eligible_universe_on(first).is_empty(), - "raw DataSet helper remains a risk-filtered universe" + dataset + .eligible_universe_on_with_risk_config(first, &selection_risk_config) + .is_empty(), + "explicit raw DataSet selection risk can still filter the universe" ); assert_eq!( manual_ctx @@ -5017,7 +5030,7 @@ mod tests { ); assert_eq!( manual_ctx - .eligible_universe_on_with_risk_config(first, &FidcRiskControlConfig::default()) + .eligible_universe_on_with_risk_config(first, &selection_risk_config) .into_iter() .map(|row| row.symbol) .collect::>(), diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 1178125..9054cf1 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -6019,7 +6019,7 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, factor_date: NaiveDate, - defer_limit_state_selection_risk: bool, + defer_all_selection_risk: bool, ) -> Vec { let mut rows = Vec::new(); for factor in ctx.data.factor_snapshots_on(factor_date) { @@ -6035,12 +6035,10 @@ impl PlatformExprStrategy { let Some(market) = ctx.data.market(date, &factor.symbol) else { continue; }; - if let Some(reason) = + if let Some(_reason) = self.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market) { - if !(defer_limit_state_selection_risk - && matches!(reason, "upper_limit" | "lower_limit")) - { + if !defer_all_selection_risk { continue; } } @@ -6081,7 +6079,7 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, factor_date: NaiveDate, - defer_limit_state_selection_risk: bool, + _defer_all_selection_risk: bool, ) -> Vec { let mut decisions = Vec::new(); for factor in ctx.data.factor_snapshots_on(factor_date) { @@ -6101,11 +6099,6 @@ impl PlatformExprStrategy { ctx.data.instrument(&factor.symbol), &self.config.risk_config, ) { - if defer_limit_state_selection_risk - && matches!(decision.rule_code.as_str(), "upper_limit" | "lower_limit") - { - continue; - } decisions.push(decision); } } @@ -6464,13 +6457,13 @@ impl PlatformExprStrategy { band_high: f64, limit: usize, ) -> Result<(Vec, Vec, Vec), BacktestError> { - let defer_limit_state_selection_risk = ctx.is_lagged_execution(); - let universe = if defer_limit_state_selection_risk { + let defer_all_selection_risk = ctx.is_lagged_execution(); + let universe = if defer_all_selection_risk { self.selectable_universe_on_with_options(ctx, date, universe_factor_date, true) } else { self.selectable_universe_on(ctx, date, universe_factor_date) }; - let risk_decisions = if defer_limit_state_selection_risk { + let risk_decisions = if defer_all_selection_risk { self.selection_risk_decisions_with_options(ctx, date, universe_factor_date, true) } else { self.selection_risk_decisions(ctx, date, universe_factor_date) @@ -8950,8 +8943,22 @@ mod tests { let default_strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + assert_eq!( + default_strategy + .selectable_universe_on(&ctx, date, date) + .len(), + 1 + ); assert!( default_strategy + .selection_risk_decisions(&ctx, date, date) + .is_empty() + ); + let mut selection_cfg = PlatformExprStrategyConfig::microcap_rotation(); + selection_cfg.risk_config.static_rules.reject_kcb_selection = true; + let selection_strategy = PlatformExprStrategy::new(selection_cfg); + assert!( + selection_strategy .selectable_universe_on(&ctx, date, date) .is_empty() ); @@ -8962,11 +8969,13 @@ mod tests { date, 5, ); - assert!( - risk_diagnostics - .iter() - .any(|item| item.contains("risk_decisions selection_total=1 by_rule=kcb:1")), - "{risk_diagnostics:?}" + assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}"); + let risk_decisions = selection_strategy.selection_risk_decisions(&ctx, date, date); + let risk_diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics( + &risk_decisions, + date, + date, + 5, ); assert!( risk_diagnostics @@ -9118,12 +9127,41 @@ mod tests { let default_strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + assert_eq!( + default_strategy + .selectable_universe_on(&ctx, date, date) + .len(), + symbols.len() + ); assert!( default_strategy + .selection_risk_decisions(&ctx, date, date) + .is_empty() + ); + let mut strict_cfg = PlatformExprStrategyConfig::microcap_rotation(); + strict_cfg + .risk_config + .static_rules + .reject_upper_limit_selection = true; + strict_cfg + .risk_config + .static_rules + .reject_lower_limit_selection = true; + strict_cfg + .risk_config + .static_rules + .reject_new_listing_selection = true; + strict_cfg + .risk_config + .static_rules + .reject_inactive_selection = true; + let strict_strategy = PlatformExprStrategy::new(strict_cfg); + assert!( + strict_strategy .selectable_universe_on(&ctx, date, date) .is_empty() ); - let rejected_rules = default_strategy + let rejected_rules = strict_strategy .selection_risk_decisions(&ctx, date, date) .into_iter() .map(|decision| (decision.symbol, decision.rule_code)) @@ -9254,10 +9292,11 @@ mod tests { let default_strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); - assert!( + assert_eq!( default_strategy .selectable_universe_on(&ctx, date, date) - .is_empty() + .len(), + 1 ); let default_stock = default_strategy .stock_state(&ctx, date, symbol) @@ -15239,6 +15278,8 @@ mod tests { cfg.selection_limit_expr = "1".to_string(); cfg.stock_filter_expr = "close > 0".to_string(); cfg.exposure_expr = "1.0".to_string(); + cfg.risk_config.static_rules.reject_st_selection = true; + cfg.risk_config.static_rules.reject_paused_selection = true; let mut strategy = PlatformExprStrategy::new(cfg); let decision = strategy.on_day(&ctx).expect("platform decision"); @@ -15247,7 +15288,7 @@ mod tests { decision .diagnostics .iter() - .any(|item| item == "selected_symbols="), + .any(|item| item == "selected_symbols=600462.SH"), "{:?}", decision.diagnostics ); @@ -15260,11 +15301,11 @@ mod tests { decision.diagnostics ); assert!( - decision.order_intents.iter().all(|intent| !matches!( + decision.order_intents.iter().any(|intent| matches!( intent, OrderIntent::Shares { symbol, quantity, .. } if symbol == candidate_symbol && *quantity > 0 - ) && !matches!( + ) || matches!( intent, OrderIntent::Value { symbol, value, .. } if symbol == candidate_symbol && *value > 0.0 @@ -15695,7 +15736,7 @@ mod tests { } #[test] - fn platform_strategy_baseline_risk_filter_cannot_be_disabled_by_empty_exclude() { + fn platform_strategy_empty_exclude_keeps_buy_risk_without_selection_filtering() { let date = d(2024, 12, 31); let symbol = "002494.SZ"; let data = DataSet::from_components( @@ -15796,7 +15837,8 @@ mod tests { let selected = strategy.selectable_universe_on(&ctx, date, date); assert_eq!(rejection.as_deref(), Some("paused")); - assert!(selected.is_empty()); + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].symbol, symbol); } fn sample_calendar() -> TradingCalendar { diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 4d8b185..67ee2e3 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -45,27 +45,27 @@ pub struct StaticRiskRuleConfig { impl Default for StaticRiskRuleConfig { fn default() -> Self { Self { - reject_st_selection: true, + reject_st_selection: false, reject_st_buy: true, - reject_star_st_selection: true, + reject_star_st_selection: false, reject_star_st_buy: true, - reject_paused_selection: true, + reject_paused_selection: false, reject_paused_buy: true, reject_paused_sell: true, - reject_inactive_selection: true, + reject_inactive_selection: false, reject_inactive_buy: true, reject_inactive_sell: true, - reject_new_listing_selection: true, + reject_new_listing_selection: false, reject_new_listing_buy: true, - reject_kcb_selection: true, + reject_kcb_selection: false, reject_kcb_buy: true, - reject_bjse_selection: true, + reject_bjse_selection: false, reject_bjse_buy: true, - reject_one_yuan_selection: true, + reject_one_yuan_selection: false, reject_one_yuan_buy: true, respect_allow_buy_sell: true, - reject_upper_limit_selection: true, - reject_lower_limit_selection: true, + reject_upper_limit_selection: false, + reject_lower_limit_selection: false, reject_upper_limit_buy: true, reject_lower_limit_sell: true, forbid_same_day_rebuy_after_sell: true, @@ -255,11 +255,6 @@ impl ChinaAShareRiskControl { ) { return Some(reason); } - if config.static_rules.respect_allow_buy_sell - && (!candidate.allow_buy || !candidate.allow_sell) - { - return Some("trade_disabled"); - } let selection_price = market.price(PriceField::Last); if config.static_rules.reject_upper_limit_selection && market.is_at_upper_limit_price(selection_price) @@ -719,7 +714,6 @@ fn missing_single_field_rejected( || config.static_rules.reject_one_yuan_selection || config.static_rules.reject_upper_limit_selection || config.static_rules.reject_lower_limit_selection - || config.static_rules.respect_allow_buy_sell } RiskCheckScope::Buy => { config.static_rules.reject_st_buy @@ -893,13 +887,19 @@ mod tests { let default_reason = ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_kcb_selection = true; + let enabled_selection_reason = + ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); config.static_rules.reject_kcb_selection = false; config.static_rules.reject_kcb_buy = false; let configured_reason = ChinaAShareRiskControl::buy_rejection_reason_with_config( date, &candidate, &market, None, 6.27, &config, ); - assert_eq!(default_reason, Some("kcb")); + assert_eq!(default_reason, None); + assert_eq!(enabled_selection_reason, Some("kcb")); assert_eq!(configured_reason, None); } @@ -1008,6 +1008,10 @@ mod tests { let default_buy = ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27); let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_bjse_selection = true; + let enabled_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); config.static_rules.reject_bjse_selection = false; config.static_rules.reject_bjse_buy = false; let configured_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config( @@ -1017,8 +1021,9 @@ mod tests { date, &candidate, &market, None, 6.27, &config, ); - assert_eq!(default_selection, Some("bjse")); + assert_eq!(default_selection, None); assert_eq!(default_buy, Some("bjse")); + assert_eq!(enabled_selection, Some("bjse")); assert_eq!(configured_selection, None); assert_eq!(configured_buy, None); } @@ -1030,13 +1035,11 @@ mod tests { candidate.symbol = "688506.SH".to_string(); candidate.risk_level_code = Some("missing_risk_state".to_string()); let market = market(date, 6.27, 5.63); + let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_kcb_selection = true; let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config( - date, - &candidate, - &market, - None, - &FidcRiskControlConfig::default(), + date, &candidate, &market, None, &config, ) .expect("kcb selection rejection"); @@ -1051,9 +1054,13 @@ mod tests { candidate.allow_sell = true; candidate.risk_level_code = Some("inactive_or_delisted".to_string()); let market = market(date, 6.27, 5.63); + let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_inactive_selection = true; assert_eq!( - ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None), + ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config + ), Some("inactive_or_delisted") ); assert_eq!( @@ -1189,6 +1196,8 @@ mod tests { Some("missing_risk_state:upper_limit_price,lower_limit_price".to_string()); let market = market(date, 6.27, 5.63); let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_upper_limit_selection = true; + config.static_rules.reject_lower_limit_selection = true; config.static_rules.reject_upper_limit_selection = false; let upper_disabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( @@ -1282,12 +1291,17 @@ mod tests { let default_reason = ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_upper_limit_selection = true; + let enabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); config.static_rules.reject_upper_limit_selection = false; let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( date, &candidate, &market, None, &config, ); - assert_eq!(default_reason, Some("upper_limit")); + assert_eq!(default_reason, None); + assert_eq!(enabled_reason, Some("upper_limit")); assert_eq!(configured_reason, None); } @@ -1300,12 +1314,17 @@ mod tests { let default_reason = ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_lower_limit_selection = true; + let enabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); config.static_rules.reject_lower_limit_selection = false; let configured_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( date, &candidate, &market, None, &config, ); - assert_eq!(default_reason, Some("lower_limit")); + assert_eq!(default_reason, None); + assert_eq!(enabled_reason, Some("lower_limit")); assert_eq!(configured_reason, None); } } diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 7a17c48..9da60b1 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -2978,30 +2978,25 @@ mod tests { let (default_selected, _) = default_strategy .select_symbols(&ctx, dates[2], 0.0, 100.0) .expect("default selection"); - assert!(default_selected.is_empty()); + assert_eq!(default_selected, vec![symbol.to_string()]); let default_risk_decisions = default_strategy.selection_risk_decisions(&ctx, dates[2]); - assert_eq!(default_risk_decisions.len(), 1); - assert_eq!(default_risk_decisions[0].symbol, symbol); - assert_eq!(default_risk_decisions[0].rule_code, "kcb"); + assert!(default_risk_decisions.is_empty()); + + let mut cfg = default_cfg; + cfg.risk_config.static_rules.reject_kcb_selection = true; + let strict_strategy = OmniMicroCapStrategy::new(cfg); + let (selected, _) = strict_strategy + .select_symbols(&ctx, dates[2], 0.0, 100.0) + .expect("strict selection"); + assert!(selected.is_empty()); + let strict_risk_decisions = strict_strategy.selection_risk_decisions(&ctx, dates[2]); + assert_eq!(strict_risk_decisions.len(), 1); + assert_eq!(strict_risk_decisions[0].symbol, symbol); + assert_eq!(strict_risk_decisions[0].rule_code, "kcb"); assert!( - default_risk_decisions[0] + strict_risk_decisions[0] .diagnostic_line() .starts_with("risk_decision=") ); - - let mut cfg = default_cfg; - cfg.risk_config.static_rules.reject_kcb_selection = false; - cfg.risk_config.static_rules.reject_kcb_buy = false; - let configured_strategy = OmniMicroCapStrategy::new(cfg); - let (selected, _) = configured_strategy - .select_symbols(&ctx, dates[2], 0.0, 100.0) - .expect("configured selection"); - assert!( - configured_strategy - .selection_risk_decisions(&ctx, dates[2]) - .is_empty() - ); - - assert_eq!(selected, vec![symbol.to_string()]); } } diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index a024d3b..8f7cdfe 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -119,8 +119,8 @@ pub struct StrategyAiOptimizeRequest { } const DEFAULT_THREE_YEAR_RETURN_TARGET_PROMPT: &str = "默认收益目标:用户没有明确指定更高收益阈值时,三年回测区间策略总收益 >= 150% 即视为满足收益目标;达到该阈值后可以继续优化夏普、回撤、换手和稳定性,但不得把已达标策略判为失败或为了追更高收益破坏无未来数据、持仓数量和同条件对账合同。"; -const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "reject_st_selection=true、reject_st_buy=true、reject_star_st_selection=true、reject_star_st_buy=true、reject_paused_selection=true、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=true、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=true、reject_new_listing_buy=true、reject_kcb_selection=true、reject_kcb_buy=true、reject_bjse_selection=true、reject_bjse_buy=true、reject_one_yuan_selection=true、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=true、reject_lower_limit_selection=true、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\""; -const DEFAULT_RISK_POLICY_DSL_CODE: &str = "reject_st_selection=true, reject_st_buy=true, reject_star_st_selection=true, reject_star_st_buy=true, reject_paused_selection=true, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=true, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=true, reject_new_listing_buy=true, reject_kcb_selection=true, reject_kcb_buy=true, reject_bjse_selection=true, reject_bjse_buy=true, reject_one_yuan_selection=true, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=true, reject_lower_limit_selection=true, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\""; +const DEFAULT_RISK_POLICY_DSL_PROMPT: &str = "reject_st_selection=false、reject_st_buy=true、reject_star_st_selection=false、reject_star_st_buy=true、reject_paused_selection=false、reject_paused_buy=true、reject_paused_sell=true、reject_inactive_selection=false、reject_inactive_buy=true、reject_inactive_sell=true、reject_new_listing_selection=false、reject_new_listing_buy=true、reject_kcb_selection=false、reject_kcb_buy=true、reject_bjse_selection=false、reject_bjse_buy=true、reject_one_yuan_selection=false、reject_one_yuan_buy=true、respect_allow_buy_sell=true、reject_upper_limit_selection=false、reject_lower_limit_selection=false、reject_upper_limit_buy=true、reject_lower_limit_sell=true、forbid_same_day_rebuy_after_sell=true、blacklist_enabled=true、volume_limit_enabled=true、liquidity_limit_enabled=true、volume_percent=0.25、commission_rate=0.0003、minimum_commission=5、stamp_tax_rate_before_change=0.001、stamp_tax_rate_after_change=0.0005、stamp_tax_change_date=\"2023-08-28\""; +const DEFAULT_RISK_POLICY_DSL_CODE: &str = "reject_st_selection=false, reject_st_buy=true, reject_star_st_selection=false, reject_star_st_buy=true, reject_paused_selection=false, reject_paused_buy=true, reject_paused_sell=true, reject_inactive_selection=false, reject_inactive_buy=true, reject_inactive_sell=true, reject_new_listing_selection=false, reject_new_listing_buy=true, reject_kcb_selection=false, reject_kcb_buy=true, reject_bjse_selection=false, reject_bjse_buy=true, reject_one_yuan_selection=false, reject_one_yuan_buy=true, respect_allow_buy_sell=true, reject_upper_limit_selection=false, reject_lower_limit_selection=false, reject_upper_limit_buy=true, reject_lower_limit_sell=true, forbid_same_day_rebuy_after_sell=true, blacklist_enabled=true, volume_limit_enabled=true, liquidity_limit_enabled=true, volume_percent=0.25, commission_rate=0.0003, minimum_commission=5, stamp_tax_rate_before_change=0.001, stamp_tax_rate_after_change=0.0005, stamp_tax_change_date=\"2023-08-28\""; pub fn built_in_strategy_manual() -> StrategyAiManual { StrategyAiManual { diff --git a/crates/fidc-core/src/universe.rs b/crates/fidc-core/src/universe.rs index bcfcd9e..1c132d7 100644 --- a/crates/fidc-core/src/universe.rs +++ b/crates/fidc-core/src/universe.rs @@ -396,13 +396,16 @@ mod tests { ) .unwrap(); let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0); + let mut risk_config = FidcRiskControlConfig::default(); + risk_config.static_rules.reject_st_selection = true; + risk_config.static_rules.reject_kcb_selection = true; let (_selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext { decision_date: d(), benchmark: &benchmark(), reference_level: 2000.0, data: &data, dynamic_universe: None, - risk_config: Some(&FidcRiskControlConfig::default()), + risk_config: Some(&risk_config), }); let rules = diagnostics @@ -450,13 +453,16 @@ mod tests { ) .unwrap(); let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0); + let mut risk_config = FidcRiskControlConfig::default(); + risk_config.static_rules.reject_st_selection = true; + risk_config.static_rules.reject_kcb_selection = true; let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext { decision_date: d(), benchmark: &benchmark(), reference_level: 2000.0, data: &data, dynamic_universe: None, - risk_config: Some(&FidcRiskControlConfig::default()), + risk_config: Some(&risk_config), }); let selected_symbols = selected