diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 89509df..22e54ad 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -185,6 +185,7 @@ pub struct BrokerSimulator { liquidity_limit: bool, strict_value_budget: bool, rebalance_cash_mode: RebalanceCashMode, + sell_then_buy_delay_slippage_rate: f64, aiquant_execution_rules: bool, same_day_buy_close_mark_at_fill: bool, risk_config: FidcRiskControlConfig, @@ -213,6 +214,7 @@ impl BrokerSimulator { liquidity_limit: true, strict_value_budget: false, rebalance_cash_mode: RebalanceCashMode::default(), + sell_then_buy_delay_slippage_rate: 0.0, aiquant_execution_rules: false, same_day_buy_close_mark_at_fill: false, risk_config: FidcRiskControlConfig::default(), @@ -245,6 +247,7 @@ impl BrokerSimulator { liquidity_limit: true, strict_value_budget: false, rebalance_cash_mode: RebalanceCashMode::default(), + sell_then_buy_delay_slippage_rate: 0.0, aiquant_execution_rules: false, same_day_buy_close_mark_at_fill: false, risk_config: FidcRiskControlConfig::default(), @@ -284,6 +287,15 @@ impl BrokerSimulator { self } + pub fn with_sell_then_buy_delay_slippage_rate(mut self, rate: f64) -> Self { + self.sell_then_buy_delay_slippage_rate = if rate.is_finite() && rate > 0.0 { + rate.min(0.999_999) + } else { + 0.0 + }; + self + } + pub fn with_aiquant_execution_rules(mut self, enabled: bool) -> Self { self.aiquant_execution_rules = enabled; self @@ -533,7 +545,7 @@ where } let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64)); - let adjusted = match self.slippage_model { + let mut adjusted = match self.slippage_model { SlippageModel::None => raw_price, SlippageModel::PriceRatio(ratio) => { let ratio = ratio.max(0.0); @@ -559,6 +571,12 @@ where } } }; + if side == OrderSide::Buy + && self.effective_rebalance_cash_mode() == RebalanceCashMode::SellThenBuy + && self.sell_then_buy_delay_slippage_rate > 0.0 + { + adjusted *= 1.0 + self.sell_then_buy_delay_slippage_rate; + } self.clamp_execution_price(snapshot, side, adjusted) } diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 43c46e1..b66d56b 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -241,6 +241,7 @@ pub struct PlatformExprStrategyConfig { pub stamp_tax_change_date: Option, pub strict_value_budget: bool, pub rebalance_cash_mode: RebalanceCashMode, + pub sell_then_buy_delay_slippage_rate: f64, pub risk_config: FidcRiskControlConfig, pub slippage_model: SlippageModel, pub matching_type: MatchingType, @@ -310,6 +311,7 @@ fn band_low(index_close) { stamp_tax_change_date: None, strict_value_budget: false, rebalance_cash_mode: RebalanceCashMode::default(), + sell_then_buy_delay_slippage_rate: 0.0, risk_config: FidcRiskControlConfig::default(), slippage_model: SlippageModel::None, matching_type: MatchingType::MinuteLast, @@ -1459,7 +1461,7 @@ impl PlatformExprStrategy { return raw_price; } let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64)); - let adjusted = match self.config.slippage_model { + let mut adjusted = match self.config.slippage_model { SlippageModel::None | SlippageModel::LimitPrice => raw_price, SlippageModel::PriceRatio(ratio) => { let ratio = ratio.max(0.0); @@ -1484,6 +1486,12 @@ impl PlatformExprStrategy { } } }; + if side == OrderSide::Buy + && self.effective_rebalance_cash_mode() == RebalanceCashMode::SellThenBuy + && self.config.sell_then_buy_delay_slippage_rate > 0.0 + { + adjusted *= 1.0 + self.config.sell_then_buy_delay_slippage_rate; + } Self::projected_clamp_execution_price(market, side, adjusted) } diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index db30201..985780e 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -107,6 +107,8 @@ pub struct StrategyExecutionSpec { pub strict_value_budget: Option, #[serde(default, alias = "rebalance_cash_mode")] pub rebalance_cash_mode: Option, + #[serde(default, alias = "sell_then_buy_delay_slippage_rate")] + pub sell_then_buy_delay_slippage_rate: Option, } #[derive(Debug, Clone, Default, Deserialize, Serialize)] @@ -179,6 +181,8 @@ pub struct StrategyEngineConfig { pub strict_value_budget: Option, #[serde(default, alias = "rebalance_cash_mode")] pub rebalance_cash_mode: Option, + #[serde(default, alias = "sell_then_buy_delay_slippage_rate")] + pub sell_then_buy_delay_slippage_rate: Option, #[serde(default)] pub dividend_reinvestment: Option, #[serde(default)] @@ -1078,6 +1082,7 @@ fn apply_execution_behavior_overrides( slippage_impact_coefficient: Option, slippage_volatility_coefficient: Option, slippage_max_value: Option, + sell_then_buy_delay_slippage_rate: Option, strict_value_budget: Option, ) -> Result<(), String> { if let Some(matching_type) = parse_matching_type(matching_type)? { @@ -1108,6 +1113,14 @@ fn apply_execution_behavior_overrides( if let Some(enabled) = strict_value_budget { cfg.strict_value_budget = enabled; } + if let Some(rate) = sell_then_buy_delay_slippage_rate { + if !rate.is_finite() || !(0.0..1.0).contains(&rate) { + return Err( + "sellThenBuyDelaySlippageRate must be a finite number in [0, 1)".to_string(), + ); + } + cfg.sell_then_buy_delay_slippage_rate = rate; + } Ok(()) } @@ -1379,6 +1392,7 @@ pub fn platform_expr_config_from_spec( engine.slippage_impact_coefficient, engine.slippage_volatility_coefficient, engine.slippage_max_value, + engine.sell_then_buy_delay_slippage_rate, engine.strict_value_budget, )?; } @@ -1792,6 +1806,7 @@ pub fn platform_expr_config_from_spec( execution.slippage_impact_coefficient, execution.slippage_volatility_coefficient, execution.slippage_max_value, + execution.sell_then_buy_delay_slippage_rate, execution.strict_value_budget, )?; sync_quote_quantity_limit(&mut cfg); @@ -2669,13 +2684,15 @@ mod tests { let spec = serde_json::json!({ "execution": { "matchingType": "next_bar_open", - "rebalanceCashMode": "pre_open_cash" + "rebalanceCashMode": "pre_open_cash", + "sellThenBuyDelaySlippageRate": 0.003 } }); let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); assert_eq!(cfg.matching_type, MatchingType::NextBarOpen); assert_eq!(cfg.rebalance_cash_mode, RebalanceCashMode::PreOpenCash); + assert_eq!(cfg.sell_then_buy_delay_slippage_rate, 0.003); let minute_spec = serde_json::json!({ "execution": { diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index 36eb908..7f0ddcf 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -258,7 +258,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { }, ManualSection { title: "execution.matching_type / execution.slippage".to_string(), - detail: "设置回测全局撮合模式和滑点。日线回测只允许 execution.matching_type(\"current_bar_close\") 或 execution.matching_type(\"next_bar_open\");current_bar_close 使用决策日当日 close,next_bar_open 使用决策日信号并在下一可交易日 open 撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;next_bar_open 的涨停买入和跌停卖出判断必须比较实际 open 成交价与涨跌停价,不能用执行日 close/last 或 next-close。分钟线回测使用当前分钟价格成交,只能写 execution.matching_type(\"minute_last\");不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type,这些只属于显式订单或内部撮合能力。滑点支持 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(\"current_bar_close\") 或 execution.matching_type(\"next_bar_open\");current_bar_close 使用决策日当日 close,next_bar_open 使用决策日信号并在下一可交易日 open 撮合,禁止把执行日 open/high/low/close 解释为下单前已知数据;next_bar_open 的涨停买入和跌停卖出判断必须比较实际 open 成交价与涨跌停价,不能用执行日 close/last 或 next-close。分钟线回测使用当前分钟价格成交,只能写 execution.matching_type(\"minute_last\");不要把 vwap、twap、open_auction、minute_best_own、minute_best_counterparty 写成全局 matching_type,这些只属于显式订单或内部撮合能力。日线调仓现金口径由 execution.rebalance_cash_mode(\"sell_then_buy\" | \"same_point_net\" | \"pre_open_cash\") 或页面/API 参数控制,默认 sell_then_buy;sell_then_buy_delay_slippage_rate 只来自页面/API 执行参数,默认 0,不要写进策略表达式。滑点支持 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(), @@ -470,7 +470,7 @@ pub fn render_manual_markdown(manual: &StrategyAiManual) -> String { out.push_str("## AI 代码生成硬约束\n"); out.push_str("- 只输出完整 `engine-script` 代码;第一行必须是 `strategy(\"...\")`、`let`、`fn`、`const` 或 `//`。\n"); out.push_str("- 禁止输出 Markdown、解释、推理过程、JSON 包装、手册复述或结果报告。\n"); - out.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`、`risk.policy`、`risk.blacklist`、`execution.matching_type`、`execution.slippage`、`universe.exclude`。\n"); + out.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`、`risk.policy`、`risk.blacklist`、`execution.matching_type`、`execution.rebalance_cash_mode`、`execution.slippage`、`universe.exclude`。\n"); out.push_str("- `universe.exclude` 只用于用户明确要求的业务排除项;ST、停牌、退市、新股、科创、一元、涨跌停、同日卖出禁买、成交量、手续费和印花税等基础风控必须写 `risk.policy(...)` 或由运行态 RiskLimits 注入。\n"); out.push_str("- 禁止伪 DSL:`filter(...)`、`rank(...)`、`select.top(...)`、`weight.equal(...)`、`sell_rule(...)`、`backtest(...)`、`risk.max_position(...)`。\n"); out.push_str("- 市值表达式字段只能用 `market_cap` 或 `free_float_cap`;不要使用数据库原始字段 `float_market_cap`。\n");