From 81f6b7d1a5977698deb5b7ff219feef411abcffa Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 6 Sep 2026 22:38:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=98=BE=E5=BC=8F=E8=82=A1?= =?UTF-8?q?=E7=A5=A8=E6=9D=83=E9=87=8D=E8=A2=AB=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../fidc-core/src/platform_expr_strategy.rs | 50 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d72ff47..b4d1972 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ risk checks. - `selection.market_cap_band(...)` 动态市值带。 - `filter.stock_expr(...)` 任意指标、因子和组合选股。 - `ordering.rank_by(...)` 与 `ordering.rank_expr(...)` 排序。 -- `allocation.buy_scale(...)` 动态买入资金比例。 +- `allocation.buy_scale(...)` 相对等权槽位的个股资金倍率;显式权重可以大于 `1.0`,组合总仓位仍由 `risk.index_exposure(...)` 和严格资金预算控制。 - `risk.stop_loss(...)`、`risk.take_profit(...)` 多条件止盈止损。 - `order.*`、`cancel.*`、`update_universe(...)`、`subscribe(...)` 显式交易动作。 diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index a46cc1c..b95542b 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8187,7 +8187,16 @@ impl PlatformExprStrategy { return Ok(1.0); } match self.eval_float(ctx, &self.config.buy_scale_expr, day, Some(stock), None) { - Ok(value) => Ok(value.clamp(0.0, 1.0)), + // `buy_scale` is a relative per-slot allocation multiplier, not the + // portfolio exposure ceiling. Explicit weights can legitimately + // exceed the equal-weight slot (for example 60/40 across two + // symbols uses scales 1.2/0.8) while `risk.index_exposure` still + // caps aggregate exposure. Clamping here silently changed saved + // stock-pool weights and caused cross-framework order drift. + Ok(value) if value.is_finite() => Ok(value.max(0.0)), + Ok(value) => Err(BacktestError::Execution(format!( + "allocation.buy_scale produced a non-finite value: {value}" + ))), Err(error) if Self::is_missing_rolling_mean_error(&error) => Ok(0.0), Err(error) => Err(error), } @@ -13630,6 +13639,45 @@ mod tests { ); } + #[test] + fn allocation_buy_scale_preserves_explicit_overweight_multiplier() { + let date = d(2025, 1, 2); + let symbol = "000001.SZ"; + let data = single_symbol_platform_data(&[date], symbol); + let portfolio = PortfolioState::new(1_000_000.0); + let subscriptions = BTreeSet::new(); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 0, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let mut config = PlatformExprStrategyConfig::microcap_rotation(); + config.signal_symbol = symbol.to_string(); + config.buy_scale_expr = "1.2".to_string(); + let strategy = PlatformExprStrategy::new(config); + let day = strategy.day_state(&ctx, date).expect("day state"); + let stock = strategy + .stock_state(&ctx, date, symbol) + .expect("stock state"); + + let scale = strategy + .buy_scale(&ctx, &day, stock.as_ref()) + .expect("buy scale"); + + assert!((scale - 1.2).abs() < 1e-12, "scale={scale}"); + } + #[test] fn universe_exclude_matches_explicit_symbols_and_bjse_alias() { let excludes = vec![