修正FIDC执行日风控配置

This commit is contained in:
boris
2026-07-06 20:05:11 +08:00
parent cb189e3de4
commit 3ef4029c4a
4 changed files with 48 additions and 5 deletions
+19 -1
View File
@@ -185,6 +185,7 @@ pub struct BrokerSimulator<C, R> {
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<C, R> BrokerSimulator<C, R> {
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<C, R> BrokerSimulator<C, R> {
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<C, R> BrokerSimulator<C, R> {
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)
}
@@ -241,6 +241,7 @@ pub struct PlatformExprStrategyConfig {
pub stamp_tax_change_date: Option<NaiveDate>,
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)
}
+18 -1
View File
@@ -107,6 +107,8 @@ pub struct StrategyExecutionSpec {
pub strict_value_budget: Option<bool>,
#[serde(default, alias = "rebalance_cash_mode")]
pub rebalance_cash_mode: Option<String>,
#[serde(default, alias = "sell_then_buy_delay_slippage_rate")]
pub sell_then_buy_delay_slippage_rate: Option<f64>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
@@ -179,6 +181,8 @@ pub struct StrategyEngineConfig {
pub strict_value_budget: Option<bool>,
#[serde(default, alias = "rebalance_cash_mode")]
pub rebalance_cash_mode: Option<String>,
#[serde(default, alias = "sell_then_buy_delay_slippage_rate")]
pub sell_then_buy_delay_slippage_rate: Option<f64>,
#[serde(default)]
pub dividend_reinvestment: Option<bool>,
#[serde(default)]
@@ -1078,6 +1082,7 @@ fn apply_execution_behavior_overrides(
slippage_impact_coefficient: Option<f64>,
slippage_volatility_coefficient: Option<f64>,
slippage_max_value: Option<f64>,
sell_then_buy_delay_slippage_rate: Option<f64>,
strict_value_budget: Option<bool>,
) -> 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": {
+2 -2
View File
@@ -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 使用决策日当日 closenext_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 使用决策日当日 closenext_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_buysell_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");