支持排名缓冲换仓策略
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user