修正FIDC执行日风控配置
This commit is contained in:
@@ -185,6 +185,7 @@ pub struct BrokerSimulator<C, R> {
|
|||||||
liquidity_limit: bool,
|
liquidity_limit: bool,
|
||||||
strict_value_budget: bool,
|
strict_value_budget: bool,
|
||||||
rebalance_cash_mode: RebalanceCashMode,
|
rebalance_cash_mode: RebalanceCashMode,
|
||||||
|
sell_then_buy_delay_slippage_rate: f64,
|
||||||
aiquant_execution_rules: bool,
|
aiquant_execution_rules: bool,
|
||||||
same_day_buy_close_mark_at_fill: bool,
|
same_day_buy_close_mark_at_fill: bool,
|
||||||
risk_config: FidcRiskControlConfig,
|
risk_config: FidcRiskControlConfig,
|
||||||
@@ -213,6 +214,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
liquidity_limit: true,
|
liquidity_limit: true,
|
||||||
strict_value_budget: false,
|
strict_value_budget: false,
|
||||||
rebalance_cash_mode: RebalanceCashMode::default(),
|
rebalance_cash_mode: RebalanceCashMode::default(),
|
||||||
|
sell_then_buy_delay_slippage_rate: 0.0,
|
||||||
aiquant_execution_rules: false,
|
aiquant_execution_rules: false,
|
||||||
same_day_buy_close_mark_at_fill: false,
|
same_day_buy_close_mark_at_fill: false,
|
||||||
risk_config: FidcRiskControlConfig::default(),
|
risk_config: FidcRiskControlConfig::default(),
|
||||||
@@ -245,6 +247,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
liquidity_limit: true,
|
liquidity_limit: true,
|
||||||
strict_value_budget: false,
|
strict_value_budget: false,
|
||||||
rebalance_cash_mode: RebalanceCashMode::default(),
|
rebalance_cash_mode: RebalanceCashMode::default(),
|
||||||
|
sell_then_buy_delay_slippage_rate: 0.0,
|
||||||
aiquant_execution_rules: false,
|
aiquant_execution_rules: false,
|
||||||
same_day_buy_close_mark_at_fill: false,
|
same_day_buy_close_mark_at_fill: false,
|
||||||
risk_config: FidcRiskControlConfig::default(),
|
risk_config: FidcRiskControlConfig::default(),
|
||||||
@@ -284,6 +287,15 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
self
|
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 {
|
pub fn with_aiquant_execution_rules(mut self, enabled: bool) -> Self {
|
||||||
self.aiquant_execution_rules = enabled;
|
self.aiquant_execution_rules = enabled;
|
||||||
self
|
self
|
||||||
@@ -533,7 +545,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
|
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::None => raw_price,
|
||||||
SlippageModel::PriceRatio(ratio) => {
|
SlippageModel::PriceRatio(ratio) => {
|
||||||
let ratio = ratio.max(0.0);
|
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)
|
self.clamp_execution_price(snapshot, side, adjusted)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub stamp_tax_change_date: Option<NaiveDate>,
|
pub stamp_tax_change_date: Option<NaiveDate>,
|
||||||
pub strict_value_budget: bool,
|
pub strict_value_budget: bool,
|
||||||
pub rebalance_cash_mode: RebalanceCashMode,
|
pub rebalance_cash_mode: RebalanceCashMode,
|
||||||
|
pub sell_then_buy_delay_slippage_rate: f64,
|
||||||
pub risk_config: FidcRiskControlConfig,
|
pub risk_config: FidcRiskControlConfig,
|
||||||
pub slippage_model: SlippageModel,
|
pub slippage_model: SlippageModel,
|
||||||
pub matching_type: MatchingType,
|
pub matching_type: MatchingType,
|
||||||
@@ -310,6 +311,7 @@ fn band_low(index_close) {
|
|||||||
stamp_tax_change_date: None,
|
stamp_tax_change_date: None,
|
||||||
strict_value_budget: false,
|
strict_value_budget: false,
|
||||||
rebalance_cash_mode: RebalanceCashMode::default(),
|
rebalance_cash_mode: RebalanceCashMode::default(),
|
||||||
|
sell_then_buy_delay_slippage_rate: 0.0,
|
||||||
risk_config: FidcRiskControlConfig::default(),
|
risk_config: FidcRiskControlConfig::default(),
|
||||||
slippage_model: SlippageModel::None,
|
slippage_model: SlippageModel::None,
|
||||||
matching_type: MatchingType::MinuteLast,
|
matching_type: MatchingType::MinuteLast,
|
||||||
@@ -1459,7 +1461,7 @@ impl PlatformExprStrategy {
|
|||||||
return raw_price;
|
return raw_price;
|
||||||
}
|
}
|
||||||
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
|
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::None | SlippageModel::LimitPrice => raw_price,
|
||||||
SlippageModel::PriceRatio(ratio) => {
|
SlippageModel::PriceRatio(ratio) => {
|
||||||
let ratio = ratio.max(0.0);
|
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)
|
Self::projected_clamp_execution_price(market, side, adjusted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ pub struct StrategyExecutionSpec {
|
|||||||
pub strict_value_budget: Option<bool>,
|
pub strict_value_budget: Option<bool>,
|
||||||
#[serde(default, alias = "rebalance_cash_mode")]
|
#[serde(default, alias = "rebalance_cash_mode")]
|
||||||
pub rebalance_cash_mode: Option<String>,
|
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)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
@@ -179,6 +181,8 @@ pub struct StrategyEngineConfig {
|
|||||||
pub strict_value_budget: Option<bool>,
|
pub strict_value_budget: Option<bool>,
|
||||||
#[serde(default, alias = "rebalance_cash_mode")]
|
#[serde(default, alias = "rebalance_cash_mode")]
|
||||||
pub rebalance_cash_mode: Option<String>,
|
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)]
|
#[serde(default)]
|
||||||
pub dividend_reinvestment: Option<bool>,
|
pub dividend_reinvestment: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -1078,6 +1082,7 @@ fn apply_execution_behavior_overrides(
|
|||||||
slippage_impact_coefficient: Option<f64>,
|
slippage_impact_coefficient: Option<f64>,
|
||||||
slippage_volatility_coefficient: Option<f64>,
|
slippage_volatility_coefficient: Option<f64>,
|
||||||
slippage_max_value: Option<f64>,
|
slippage_max_value: Option<f64>,
|
||||||
|
sell_then_buy_delay_slippage_rate: Option<f64>,
|
||||||
strict_value_budget: Option<bool>,
|
strict_value_budget: Option<bool>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(matching_type) = parse_matching_type(matching_type)? {
|
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 {
|
if let Some(enabled) = strict_value_budget {
|
||||||
cfg.strict_value_budget = enabled;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,6 +1392,7 @@ pub fn platform_expr_config_from_spec(
|
|||||||
engine.slippage_impact_coefficient,
|
engine.slippage_impact_coefficient,
|
||||||
engine.slippage_volatility_coefficient,
|
engine.slippage_volatility_coefficient,
|
||||||
engine.slippage_max_value,
|
engine.slippage_max_value,
|
||||||
|
engine.sell_then_buy_delay_slippage_rate,
|
||||||
engine.strict_value_budget,
|
engine.strict_value_budget,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
@@ -1792,6 +1806,7 @@ pub fn platform_expr_config_from_spec(
|
|||||||
execution.slippage_impact_coefficient,
|
execution.slippage_impact_coefficient,
|
||||||
execution.slippage_volatility_coefficient,
|
execution.slippage_volatility_coefficient,
|
||||||
execution.slippage_max_value,
|
execution.slippage_max_value,
|
||||||
|
execution.sell_then_buy_delay_slippage_rate,
|
||||||
execution.strict_value_budget,
|
execution.strict_value_budget,
|
||||||
)?;
|
)?;
|
||||||
sync_quote_quantity_limit(&mut cfg);
|
sync_quote_quantity_limit(&mut cfg);
|
||||||
@@ -2669,13 +2684,15 @@ mod tests {
|
|||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
"execution": {
|
"execution": {
|
||||||
"matchingType": "next_bar_open",
|
"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");
|
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
|
||||||
|
|
||||||
assert_eq!(cfg.matching_type, MatchingType::NextBarOpen);
|
assert_eq!(cfg.matching_type, MatchingType::NextBarOpen);
|
||||||
assert_eq!(cfg.rebalance_cash_mode, RebalanceCashMode::PreOpenCash);
|
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!({
|
let minute_spec = serde_json::json!({
|
||||||
"execution": {
|
"execution": {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
},
|
},
|
||||||
ManualSection {
|
ManualSection {
|
||||||
title: "execution.matching_type / execution.slippage".to_string(),
|
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 {
|
ManualSection {
|
||||||
title: "期货提交校验".to_string(),
|
title: "期货提交校验".to_string(),
|
||||||
@@ -470,7 +470,7 @@ pub fn render_manual_markdown(manual: &StrategyAiManual) -> String {
|
|||||||
out.push_str("## AI 代码生成硬约束\n");
|
out.push_str("## AI 代码生成硬约束\n");
|
||||||
out.push_str("- 只输出完整 `engine-script` 代码;第一行必须是 `strategy(\"...\")`、`let`、`fn`、`const` 或 `//`。\n");
|
out.push_str("- 只输出完整 `engine-script` 代码;第一行必须是 `strategy(\"...\")`、`let`、`fn`、`const` 或 `//`。\n");
|
||||||
out.push_str("- 禁止输出 Markdown、解释、推理过程、JSON 包装、手册复述或结果报告。\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("- `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("- 禁止伪 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");
|
out.push_str("- 市值表达式字段只能用 `market_cap` 或 `free_float_cap`;不要使用数据库原始字段 `float_market_cap`。\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user