隔离退市持仓与模型目标状态

This commit is contained in:
boris
2026-07-18 18:20:26 +08:00
parent 117f7be9c8
commit a77a00c70a
+11 -62
View File
@@ -986,25 +986,6 @@ impl PlatformExprStrategy {
.collect()
}
fn reserve_unresolved_delisted_slots(
ranked_selection: Vec<String>,
unresolved_symbols: &BTreeSet<String>,
selection_limit: usize,
) -> Vec<String> {
if selection_limit == 0 || unresolved_symbols.is_empty() {
return ranked_selection;
}
let reserved_count = unresolved_symbols.len().min(selection_limit);
let active_limit = selection_limit.saturating_sub(reserved_count);
let mut selected = ranked_selection
.into_iter()
.filter(|symbol| !unresolved_symbols.contains(symbol))
.take(active_limit)
.collect::<Vec<_>>();
selected.extend(unresolved_symbols.iter().take(reserved_count).cloned());
selected
}
fn effective_rebalance_cash_mode(&self) -> RebalanceCashMode {
if self.config.matching_type == MatchingType::MinuteLast {
RebalanceCashMode::SellThenBuy
@@ -9262,7 +9243,7 @@ impl Strategy for PlatformExprStrategy {
let mut risk_decisions = Vec::new();
let mut replacement_reference_source = "disabled";
let mut replacement_reference_count = 0usize;
let mut stock_list = if self.config.rotation_enabled && !in_skip_window {
let stock_list = if self.config.rotation_enabled && !in_skip_window {
let selection_buffer_rank =
self.selection_candidate_limit(ctx, &day, selection_limit)?;
let ranked_selection_limit = if self.config.daily_replacement_limit > 0 {
@@ -9319,14 +9300,9 @@ impl Strategy for PlatformExprStrategy {
};
let unresolved_delisted_symbols = Self::unresolved_delisted_position_symbols(ctx);
if !unresolved_delisted_symbols.is_empty() {
stock_list = Self::reserve_unresolved_delisted_slots(
stock_list,
&unresolved_delisted_symbols,
selection_limit,
);
selection_notes.push(format!(
"unresolved_delisted_slots reserved={} symbols={} settlement_action=missing valuation_policy=zero no_order=true",
unresolved_delisted_symbols.len().min(selection_limit),
"unresolved_delisted_actual_slots reserved={} symbols={} settlement_action=missing valuation_policy=zero no_order=true model_targets_unchanged=true",
unresolved_delisted_symbols.len(),
unresolved_delisted_symbols
.iter()
.cloned()
@@ -11091,40 +11067,6 @@ mod tests {
);
}
#[test]
fn unresolved_delisted_position_reserves_target_slot_without_replacement() {
let ranked = vec![
"000001.SZ".to_string(),
"000002.SZ".to_string(),
"000003.SZ".to_string(),
"000004.SZ".to_string(),
];
let unresolved = BTreeSet::from(["000999.SZ".to_string()]);
let selected =
PlatformExprStrategy::reserve_unresolved_delisted_slots(ranked, &unresolved, 3);
assert_eq!(
selected,
vec![
"000001.SZ".to_string(),
"000002.SZ".to_string(),
"000999.SZ".to_string(),
]
);
}
#[test]
fn unresolved_delisted_positions_exhaust_target_slots_before_new_candidates() {
let ranked = vec!["000001.SZ".to_string(), "000002.SZ".to_string()];
let unresolved = BTreeSet::from(["000998.SZ".to_string(), "000999.SZ".to_string()]);
let selected =
PlatformExprStrategy::reserve_unresolved_delisted_slots(ranked, &unresolved, 1);
assert_eq!(selected, vec!["000998.SZ".to_string()]);
}
#[test]
fn platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement() {
let previous_date = d(2025, 1, 2);
@@ -11282,10 +11224,17 @@ mod tests {
let decision = strategy.on_day(&ctx).expect("platform decision");
assert!(decision.diagnostics.iter().any(|note| {
note.contains("unresolved_delisted_slots reserved=1")
note.contains("unresolved_delisted_actual_slots reserved=1")
&& note.contains(unresolved_symbol)
&& note.contains("no_order=true")
&& note.contains("model_targets_unchanged=true")
}));
assert!(
decision
.diagnostics
.iter()
.any(|note| note == "selected_symbols=000001.SZ,000002.SZ")
);
assert!(!decision.order_intents.iter().any(|intent| {
matches!(
intent,