修复显式股票权重被截断
This commit is contained in:
@@ -85,7 +85,7 @@ risk checks.
|
|||||||
- `selection.market_cap_band(...)` 动态市值带。
|
- `selection.market_cap_band(...)` 动态市值带。
|
||||||
- `filter.stock_expr(...)` 任意指标、因子和组合选股。
|
- `filter.stock_expr(...)` 任意指标、因子和组合选股。
|
||||||
- `ordering.rank_by(...)` 与 `ordering.rank_expr(...)` 排序。
|
- `ordering.rank_by(...)` 与 `ordering.rank_expr(...)` 排序。
|
||||||
- `allocation.buy_scale(...)` 动态买入资金比例。
|
- `allocation.buy_scale(...)` 相对等权槽位的个股资金倍率;显式权重可以大于 `1.0`,组合总仓位仍由 `risk.index_exposure(...)` 和严格资金预算控制。
|
||||||
- `risk.stop_loss(...)`、`risk.take_profit(...)` 多条件止盈止损。
|
- `risk.stop_loss(...)`、`risk.take_profit(...)` 多条件止盈止损。
|
||||||
- `order.*`、`cancel.*`、`update_universe(...)`、`subscribe(...)` 显式交易动作。
|
- `order.*`、`cancel.*`、`update_universe(...)`、`subscribe(...)` 显式交易动作。
|
||||||
|
|
||||||
|
|||||||
@@ -8187,7 +8187,16 @@ impl PlatformExprStrategy {
|
|||||||
return Ok(1.0);
|
return Ok(1.0);
|
||||||
}
|
}
|
||||||
match self.eval_float(ctx, &self.config.buy_scale_expr, day, Some(stock), None) {
|
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) if Self::is_missing_rolling_mean_error(&error) => Ok(0.0),
|
||||||
Err(error) => Err(error),
|
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]
|
#[test]
|
||||||
fn universe_exclude_matches_explicit_symbols_and_bjse_alias() {
|
fn universe_exclude_matches_explicit_symbols_and_bjse_alias() {
|
||||||
let excludes = vec![
|
let excludes = vec![
|
||||||
|
|||||||
Reference in New Issue
Block a user