修复显式股票权重被截断
This commit is contained in:
@@ -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(...)` 显式交易动作。
|
||||
|
||||
|
||||
@@ -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![
|
||||
|
||||
Reference in New Issue
Block a user