修正动态排名替换目标状态

This commit is contained in:
boris
2026-07-18 08:35:12 +08:00
parent 755fffda0f
commit 4c0fde7621
+82 -4
View File
@@ -799,6 +799,7 @@ pub struct PlatformExprStrategy {
engine: Engine, engine: Engine,
rebalance_day_counter: usize, rebalance_day_counter: usize,
last_rebalance_date: Option<NaiveDate>, last_rebalance_date: Option<NaiveDate>,
last_target_selection: Option<BTreeSet<String>>,
last_trading_ratio: Option<f64>, last_trading_ratio: Option<f64>,
portfolio_drawdown_controller: Option<PlatformPortfolioDrawdownController>, portfolio_drawdown_controller: Option<PlatformPortfolioDrawdownController>,
pending_highlimit_holdings: BTreeSet<String>, pending_highlimit_holdings: BTreeSet<String>,
@@ -882,6 +883,18 @@ impl PlatformExprStrategy {
defer_execution || projected_after_qty != before_qty defer_execution || projected_after_qty != before_qty
} }
fn replacement_reference_symbols(
&self,
actual_held_symbols: BTreeSet<String>,
) -> (&'static str, BTreeSet<String>) {
if self.config.daily_replacement_limit > 0 {
if let Some(last_target_selection) = &self.last_target_selection {
return ("previous_target", last_target_selection.clone());
}
}
("actual_holdings", actual_held_symbols)
}
fn buffered_selection( fn buffered_selection(
ranked_symbols: &[String], ranked_symbols: &[String],
held_symbols: &BTreeSet<String>, held_symbols: &BTreeSet<String>,
@@ -1093,6 +1106,7 @@ impl PlatformExprStrategy {
engine, engine,
rebalance_day_counter: 0, rebalance_day_counter: 0,
last_rebalance_date: None, last_rebalance_date: None,
last_target_selection: None,
last_trading_ratio: None, last_trading_ratio: None,
portfolio_drawdown_controller, portfolio_drawdown_controller,
pending_highlimit_holdings: BTreeSet::new(), pending_highlimit_holdings: BTreeSet::new(),
@@ -9172,6 +9186,8 @@ impl Strategy for PlatformExprStrategy {
0 0
}; };
let mut risk_decisions = Vec::new(); let mut risk_decisions = Vec::new();
let mut replacement_reference_source = "disabled";
let mut replacement_reference_count = 0usize;
let 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)?;
@@ -9192,18 +9208,22 @@ impl Strategy for PlatformExprStrategy {
)?; )?;
selection_notes = notes; selection_notes = notes;
risk_decisions = selection_risk_decisions; risk_decisions = selection_risk_decisions;
let mut held_symbols = ctx let mut actual_held_symbols = ctx
.portfolio .portfolio
.positions() .positions()
.keys() .keys()
.cloned() .cloned()
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
if persistent_model_lifecycle { if persistent_model_lifecycle {
held_symbols.extend(self.position_entry_dates.keys().cloned()); actual_held_symbols.extend(self.position_entry_dates.keys().cloned());
} }
let (reference_source, replacement_reference_symbols) =
self.replacement_reference_symbols(actual_held_symbols);
replacement_reference_source = reference_source;
replacement_reference_count = replacement_reference_symbols.len();
let selected = Self::buffered_selection( let selected = Self::buffered_selection(
&ranked_stock_list, &ranked_stock_list,
&held_symbols, &replacement_reference_symbols,
selection_limit, selection_limit,
selection_buffer_rank, selection_buffer_rank,
self.config.daily_replacement_limit, self.config.daily_replacement_limit,
@@ -10738,6 +10758,10 @@ impl Strategy for PlatformExprStrategy {
self.rebalance_day_counter = self.rebalance_day_counter.saturating_add(1); self.rebalance_day_counter = self.rebalance_day_counter.saturating_add(1);
} }
} }
if self.config.rotation_enabled && periodic_rebalance {
self.last_target_selection =
Some(stock_list.iter().take(selection_limit).cloned().collect());
}
if self.config.rotation_enabled && trading_ratio.is_finite() { if self.config.rotation_enabled && trading_ratio.is_finite() {
self.last_trading_ratio = Some(trading_ratio); self.last_trading_ratio = Some(trading_ratio);
} }
@@ -10786,13 +10810,15 @@ impl Strategy for PlatformExprStrategy {
) )
}, },
format!( format!(
"selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={} decision_date={} signal_rebalance_date_count={} signal_rebalance_match={} selection_market_date={} selection_universe_factor_date={} selection_factor_date={} execution_date={} budget_total={:.2} marked_total={:.2} day_total={:.2}", "selected={} periodic_rebalance={} exits={} projected_positions={} intents={} limit={} replacement_reference={} replacement_reference_count={} decision_date={} signal_rebalance_date_count={} signal_rebalance_match={} selection_market_date={} selection_universe_factor_date={} selection_factor_date={} execution_date={} budget_total={:.2} marked_total={:.2} day_total={:.2}",
stock_list.len(), stock_list.len(),
periodic_rebalance, periodic_rebalance,
exit_symbols.len(), exit_symbols.len(),
projected.positions().len(), projected.positions().len(),
order_intents.len(), order_intents.len(),
selection_limit, selection_limit,
replacement_reference_source,
replacement_reference_count,
decision_date, decision_date,
self.config.signal_rebalance_dates.len(), self.config.signal_rebalance_dates.len(),
self.config.signal_rebalance_dates.contains(&decision_date), self.config.signal_rebalance_dates.contains(&decision_date),
@@ -31600,6 +31626,58 @@ mod tests {
assert_eq!(held.difference(&selected_set).count(), 2); assert_eq!(held.difference(&selected_set).count(), 2);
} }
#[test]
fn daily_replacement_reference_uses_previous_target_before_lagged_orders_fill() {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.daily_replacement_limit = 2;
let mut strategy = PlatformExprStrategy::new(cfg);
let previous_target = ["000001.SZ", "000002.SZ", "000003.SZ", "000004.SZ"]
.into_iter()
.map(str::to_string)
.collect::<BTreeSet<_>>();
strategy.last_target_selection = Some(previous_target.clone());
let (source, reference) = strategy.replacement_reference_symbols(BTreeSet::new());
let ranked = [
"000005.SZ",
"000006.SZ",
"000007.SZ",
"000008.SZ",
"000001.SZ",
"000002.SZ",
"000003.SZ",
"000004.SZ",
]
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>();
let selected = PlatformExprStrategy::buffered_selection(&ranked, &reference, 4, 4, 2);
assert_eq!(source, "previous_target");
assert_eq!(reference, previous_target);
assert_eq!(
selected,
vec!["000005.SZ", "000006.SZ", "000001.SZ", "000002.SZ"]
);
}
#[test]
fn daily_replacement_reference_records_an_explicit_empty_previous_target() {
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.daily_replacement_limit = 2;
let mut strategy = PlatformExprStrategy::new(cfg);
strategy.last_target_selection = Some(BTreeSet::new());
let actual = ["000001.SZ"]
.into_iter()
.map(str::to_string)
.collect::<BTreeSet<_>>();
let (source, reference) = strategy.replacement_reference_symbols(actual);
assert_eq!(source, "previous_target");
assert!(reference.is_empty());
}
#[test] #[test]
fn lagged_rebalance_emits_target_value_even_when_projection_is_unchanged() { fn lagged_rebalance_emits_target_value_even_when_projection_is_unchanged() {
assert!(PlatformExprStrategy::should_emit_rebalance_target_value( assert!(PlatformExprStrategy::should_emit_rebalance_target_value(