支持排名缓冲换仓策略

This commit is contained in:
boris
2026-07-12 01:05:56 +08:00
parent 30a4071ee0
commit d5d67102ac
2 changed files with 84 additions and 4 deletions
+74 -4
View File
@@ -231,6 +231,7 @@ pub struct PlatformExprStrategyConfig {
pub daily_top_up_enabled: bool,
pub daily_position_target_adjust_enabled: bool,
pub rebalance_existing_positions: bool,
pub selection_buffer_multiple: f64,
pub retry_empty_rebalance: bool,
pub calendar_rebalance_interval: bool,
pub max_holding_days: Option<i64>,
@@ -303,6 +304,7 @@ fn band_low(index_close) {
daily_top_up_enabled: false,
daily_position_target_adjust_enabled: true,
rebalance_existing_positions: false,
selection_buffer_multiple: 1.0,
retry_empty_rebalance: false,
calendar_rebalance_interval: false,
max_holding_days: None,
@@ -673,6 +675,43 @@ impl PlatformExprStrategy {
value
}
fn selection_buffer_rank(&self, selection_limit: usize) -> usize {
let multiple = if self.config.selection_buffer_multiple.is_finite() {
self.config.selection_buffer_multiple.max(1.0)
} else {
1.0
};
selection_limit.max((selection_limit as f64 * multiple) as usize)
}
fn buffered_selection(
ranked_symbols: &[String],
held_symbols: &BTreeSet<String>,
selection_limit: usize,
buffer_rank: usize,
) -> Vec<String> {
if selection_limit == 0 {
return Vec::new();
}
let mut selected = ranked_symbols
.iter()
.take(buffer_rank.max(selection_limit))
.filter(|symbol| held_symbols.contains(*symbol))
.cloned()
.take(selection_limit)
.collect::<Vec<_>>();
let mut selected_set = selected.iter().cloned().collect::<BTreeSet<_>>();
for symbol in ranked_symbols {
if selected.len() >= selection_limit {
break;
}
if selected_set.insert(symbol.clone()) {
selected.push(symbol.clone());
}
}
selected
}
fn effective_rebalance_cash_mode(&self) -> RebalanceCashMode {
if self.config.matching_type == MatchingType::MinuteLast {
RebalanceCashMode::SellThenBuy
@@ -8151,6 +8190,7 @@ impl PlatformExprStrategy {
let selection_limit = self
.selection_limit(ctx, &day)?
.min(self.config.max_positions.max(1));
let quote_selection_limit = self.selection_buffer_rank(selection_limit);
let (candidate_symbols, order_symbols, processed_scope, diagnostics) = self
.select_quote_plan_symbols(
ctx,
@@ -8160,7 +8200,7 @@ impl PlatformExprStrategy {
&day,
band_low,
band_high,
selection_limit,
quote_selection_limit,
)?;
Ok(PlatformSelectionQuotePlan {
execution_date: ctx.execution_date,
@@ -8466,7 +8506,8 @@ impl Strategy for PlatformExprStrategy {
};
let mut risk_decisions = Vec::new();
let stock_list = if self.config.rotation_enabled && !in_skip_window {
let (stock_list, notes, selection_risk_decisions) = self.select_symbols(
let selection_buffer_rank = self.selection_buffer_rank(selection_limit);
let (ranked_stock_list, notes, selection_risk_decisions) = self.select_symbols(
ctx,
selection_market_date,
selection_universe_factor_date,
@@ -8474,11 +8515,22 @@ impl Strategy for PlatformExprStrategy {
&day,
band_low,
band_high,
selection_limit,
selection_buffer_rank,
)?;
selection_notes = notes;
risk_decisions = selection_risk_decisions;
stock_list
let held_symbols = ctx
.portfolio
.positions()
.keys()
.cloned()
.collect::<BTreeSet<_>>();
Self::buffered_selection(
&ranked_stock_list,
&held_symbols,
selection_limit,
selection_buffer_rank,
)
} else {
Vec::new()
};
@@ -29660,4 +29712,22 @@ mod tests {
vec!["000001.SZ".to_string(), "000003.SZ".to_string()]
);
}
#[test]
fn selection_buffer_retains_held_symbols_within_buffer_rank() {
let ranked = (1..=8)
.map(|index| format!("{index:06}.SZ"))
.collect::<Vec<_>>();
let held = ["000005.SZ", "000006.SZ"]
.into_iter()
.map(str::to_string)
.collect::<BTreeSet<_>>();
let selected = PlatformExprStrategy::buffered_selection(&ranked, &held, 4, 6);
assert_eq!(
selected,
vec!["000005.SZ", "000006.SZ", "000001.SZ", "000002.SZ"]
);
}
}
@@ -695,6 +695,8 @@ pub struct StrategyExpressionTradingConfig {
#[serde(default)]
pub rebalance_existing_positions: Option<bool>,
#[serde(default)]
pub selection_buffer_multiple: Option<f64>,
#[serde(default)]
pub retry_empty_rebalance: Option<bool>,
#[serde(default)]
pub weak_market_shrink_overweight_threshold: Option<f64>,
@@ -1638,6 +1640,12 @@ pub fn platform_expr_config_from_spec(
if let Some(enabled) = trading.rebalance_existing_positions {
cfg.rebalance_existing_positions = enabled;
}
if let Some(multiple) = trading
.selection_buffer_multiple
.filter(|value| value.is_finite() && *value >= 1.0)
{
cfg.selection_buffer_multiple = multiple;
}
if let Some(enabled) = trading.retry_empty_rebalance {
cfg.retry_empty_rebalance = enabled;
}
@@ -2957,6 +2965,7 @@ mod tests {
"dailyTopUp": false,
"dailyPositionTargetAdjust": false,
"rebalanceExistingPositions": true,
"selectionBufferMultiple": 1.5,
"retryEmptyRebalance": false
}
}
@@ -2967,6 +2976,7 @@ mod tests {
assert!(!cfg.daily_top_up_enabled);
assert!(!cfg.daily_position_target_adjust_enabled);
assert!(cfg.rebalance_existing_positions);
assert_eq!(cfg.selection_buffer_multiple, 1.5);
assert!(!cfg.retry_empty_rebalance);
}