支持排名缓冲换仓策略
This commit is contained in:
@@ -231,6 +231,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub daily_top_up_enabled: bool,
|
pub daily_top_up_enabled: bool,
|
||||||
pub daily_position_target_adjust_enabled: bool,
|
pub daily_position_target_adjust_enabled: bool,
|
||||||
pub rebalance_existing_positions: bool,
|
pub rebalance_existing_positions: bool,
|
||||||
|
pub selection_buffer_multiple: f64,
|
||||||
pub retry_empty_rebalance: bool,
|
pub retry_empty_rebalance: bool,
|
||||||
pub calendar_rebalance_interval: bool,
|
pub calendar_rebalance_interval: bool,
|
||||||
pub max_holding_days: Option<i64>,
|
pub max_holding_days: Option<i64>,
|
||||||
@@ -303,6 +304,7 @@ fn band_low(index_close) {
|
|||||||
daily_top_up_enabled: false,
|
daily_top_up_enabled: false,
|
||||||
daily_position_target_adjust_enabled: true,
|
daily_position_target_adjust_enabled: true,
|
||||||
rebalance_existing_positions: false,
|
rebalance_existing_positions: false,
|
||||||
|
selection_buffer_multiple: 1.0,
|
||||||
retry_empty_rebalance: false,
|
retry_empty_rebalance: false,
|
||||||
calendar_rebalance_interval: false,
|
calendar_rebalance_interval: false,
|
||||||
max_holding_days: None,
|
max_holding_days: None,
|
||||||
@@ -673,6 +675,43 @@ impl PlatformExprStrategy {
|
|||||||
value
|
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 {
|
fn effective_rebalance_cash_mode(&self) -> RebalanceCashMode {
|
||||||
if self.config.matching_type == MatchingType::MinuteLast {
|
if self.config.matching_type == MatchingType::MinuteLast {
|
||||||
RebalanceCashMode::SellThenBuy
|
RebalanceCashMode::SellThenBuy
|
||||||
@@ -8151,6 +8190,7 @@ impl PlatformExprStrategy {
|
|||||||
let selection_limit = self
|
let selection_limit = self
|
||||||
.selection_limit(ctx, &day)?
|
.selection_limit(ctx, &day)?
|
||||||
.min(self.config.max_positions.max(1));
|
.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
|
let (candidate_symbols, order_symbols, processed_scope, diagnostics) = self
|
||||||
.select_quote_plan_symbols(
|
.select_quote_plan_symbols(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -8160,7 +8200,7 @@ impl PlatformExprStrategy {
|
|||||||
&day,
|
&day,
|
||||||
band_low,
|
band_low,
|
||||||
band_high,
|
band_high,
|
||||||
selection_limit,
|
quote_selection_limit,
|
||||||
)?;
|
)?;
|
||||||
Ok(PlatformSelectionQuotePlan {
|
Ok(PlatformSelectionQuotePlan {
|
||||||
execution_date: ctx.execution_date,
|
execution_date: ctx.execution_date,
|
||||||
@@ -8466,7 +8506,8 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
};
|
};
|
||||||
let mut risk_decisions = Vec::new();
|
let mut risk_decisions = Vec::new();
|
||||||
let stock_list = if self.config.rotation_enabled && !in_skip_window {
|
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,
|
ctx,
|
||||||
selection_market_date,
|
selection_market_date,
|
||||||
selection_universe_factor_date,
|
selection_universe_factor_date,
|
||||||
@@ -8474,11 +8515,22 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
&day,
|
&day,
|
||||||
band_low,
|
band_low,
|
||||||
band_high,
|
band_high,
|
||||||
selection_limit,
|
selection_buffer_rank,
|
||||||
)?;
|
)?;
|
||||||
selection_notes = notes;
|
selection_notes = notes;
|
||||||
risk_decisions = selection_risk_decisions;
|
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 {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
@@ -29660,4 +29712,22 @@ mod tests {
|
|||||||
vec!["000001.SZ".to_string(), "000003.SZ".to_string()]
|
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)]
|
#[serde(default)]
|
||||||
pub rebalance_existing_positions: Option<bool>,
|
pub rebalance_existing_positions: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub selection_buffer_multiple: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
pub retry_empty_rebalance: Option<bool>,
|
pub retry_empty_rebalance: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub weak_market_shrink_overweight_threshold: Option<f64>,
|
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 {
|
if let Some(enabled) = trading.rebalance_existing_positions {
|
||||||
cfg.rebalance_existing_positions = enabled;
|
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 {
|
if let Some(enabled) = trading.retry_empty_rebalance {
|
||||||
cfg.retry_empty_rebalance = enabled;
|
cfg.retry_empty_rebalance = enabled;
|
||||||
}
|
}
|
||||||
@@ -2957,6 +2965,7 @@ mod tests {
|
|||||||
"dailyTopUp": false,
|
"dailyTopUp": false,
|
||||||
"dailyPositionTargetAdjust": false,
|
"dailyPositionTargetAdjust": false,
|
||||||
"rebalanceExistingPositions": true,
|
"rebalanceExistingPositions": true,
|
||||||
|
"selectionBufferMultiple": 1.5,
|
||||||
"retryEmptyRebalance": false
|
"retryEmptyRebalance": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2967,6 +2976,7 @@ mod tests {
|
|||||||
assert!(!cfg.daily_top_up_enabled);
|
assert!(!cfg.daily_top_up_enabled);
|
||||||
assert!(!cfg.daily_position_target_adjust_enabled);
|
assert!(!cfg.daily_position_target_adjust_enabled);
|
||||||
assert!(cfg.rebalance_existing_positions);
|
assert!(cfg.rebalance_existing_positions);
|
||||||
|
assert_eq!(cfg.selection_buffer_multiple, 1.5);
|
||||||
assert!(!cfg.retry_empty_rebalance);
|
assert!(!cfg.retry_empty_rebalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user