隔离退市持仓与模型目标状态
This commit is contained in:
@@ -986,25 +986,6 @@ impl PlatformExprStrategy {
|
|||||||
.collect()
|
.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 {
|
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
|
||||||
@@ -9262,7 +9243,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
let mut risk_decisions = Vec::new();
|
let mut risk_decisions = Vec::new();
|
||||||
let mut replacement_reference_source = "disabled";
|
let mut replacement_reference_source = "disabled";
|
||||||
let mut replacement_reference_count = 0usize;
|
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 =
|
let selection_buffer_rank =
|
||||||
self.selection_candidate_limit(ctx, &day, selection_limit)?;
|
self.selection_candidate_limit(ctx, &day, selection_limit)?;
|
||||||
let ranked_selection_limit = if self.config.daily_replacement_limit > 0 {
|
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);
|
let unresolved_delisted_symbols = Self::unresolved_delisted_position_symbols(ctx);
|
||||||
if !unresolved_delisted_symbols.is_empty() {
|
if !unresolved_delisted_symbols.is_empty() {
|
||||||
stock_list = Self::reserve_unresolved_delisted_slots(
|
|
||||||
stock_list,
|
|
||||||
&unresolved_delisted_symbols,
|
|
||||||
selection_limit,
|
|
||||||
);
|
|
||||||
selection_notes.push(format!(
|
selection_notes.push(format!(
|
||||||
"unresolved_delisted_slots reserved={} symbols={} settlement_action=missing valuation_policy=zero no_order=true",
|
"unresolved_delisted_actual_slots reserved={} symbols={} settlement_action=missing valuation_policy=zero no_order=true model_targets_unchanged=true",
|
||||||
unresolved_delisted_symbols.len().min(selection_limit),
|
unresolved_delisted_symbols.len(),
|
||||||
unresolved_delisted_symbols
|
unresolved_delisted_symbols
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.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]
|
#[test]
|
||||||
fn platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement() {
|
fn platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement() {
|
||||||
let previous_date = d(2025, 1, 2);
|
let previous_date = d(2025, 1, 2);
|
||||||
@@ -11282,10 +11224,17 @@ mod tests {
|
|||||||
let decision = strategy.on_day(&ctx).expect("platform decision");
|
let decision = strategy.on_day(&ctx).expect("platform decision");
|
||||||
|
|
||||||
assert!(decision.diagnostics.iter().any(|note| {
|
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(unresolved_symbol)
|
||||||
&& note.contains("no_order=true")
|
&& 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| {
|
assert!(!decision.order_intents.iter().any(|intent| {
|
||||||
matches!(
|
matches!(
|
||||||
intent,
|
intent,
|
||||||
|
|||||||
Reference in New Issue
Block a user