对齐ALV日内补仓执行顺序

This commit is contained in:
boris
2026-07-08 01:44:32 +08:00
parent e0a7eb8972
commit 89f8bb32d0
+244 -142
View File
@@ -2240,6 +2240,172 @@ impl PlatformExprStrategy {
per_slot_budget.min(single_slot_cap) per_slot_budget.min(single_slot_cap)
} }
#[allow(clippy::too_many_arguments)]
fn try_daily_top_up_once(
&mut self,
ctx: &StrategyContext<'_>,
day: &DayExpressionState,
stock_list: &[String],
decision_date: NaiveDate,
execution_date: NaiveDate,
projection_date: NaiveDate,
selection_factor_date: NaiveDate,
signal_date: NaiveDate,
target_budget: f64,
selection_limit: usize,
defer_execution_risk: bool,
current_position_symbol: Option<&str>,
projected: &mut PortfolioState,
projected_execution_state: &mut ProjectedExecutionState,
order_intents: &mut Vec<OrderIntent>,
aiquant_available_cash: &mut f64,
slot_working_symbols: &mut BTreeSet<String>,
same_bar_buy_symbols: &mut BTreeSet<String>,
pending_full_close_symbols: &BTreeSet<String>,
same_day_sold_symbols: &BTreeSet<String>,
exit_symbols: &BTreeSet<String>,
delayed_sold_symbols: &BTreeSet<String>,
intraday_attempted_buys: &mut BTreeSet<String>,
pending_buy_value: &mut f64,
debug_daily_top_up: bool,
daily_top_up_debug_notes: &mut Vec<String>,
) -> Result<bool, BacktestError> {
if target_budget <= 0.0 || selection_limit == 0 || stock_list.is_empty() {
return Ok(false);
}
let mut budget_working_symbols = slot_working_symbols.clone();
budget_working_symbols.extend(same_bar_buy_symbols.iter().cloned());
if Self::effective_slot_count(&budget_working_symbols, pending_full_close_symbols)
>= selection_limit
{
return Ok(false);
}
let value_symbols = budget_working_symbols.clone();
let active_value = if debug_daily_top_up {
self.remaining_buy_cash_active_value(
ctx,
projected,
projection_date,
&budget_working_symbols,
&value_symbols,
)
} else {
0.0
};
let slot_buy_cash = self.remaining_buy_cash_per_slot(
ctx,
projected,
projection_date,
target_budget,
selection_limit,
&budget_working_symbols,
&value_symbols,
*pending_buy_value,
pending_full_close_symbols,
);
let available_buy_cash = slot_buy_cash.min(*aiquant_available_cash);
if debug_daily_top_up {
daily_top_up_debug_notes.push(format!(
"daily_top_up_budget date={} working={} value_symbols={} same_bar_buys={} active_value={:.4} pending_buy_value={:.4} target_budget={:.4} slot_buy_cash={:.4} available_cash={:.4} portfolio_cash={:.4}",
execution_date,
budget_working_symbols.len(),
value_symbols.len(),
same_bar_buy_symbols.len(),
active_value,
*pending_buy_value,
target_budget,
slot_buy_cash,
available_buy_cash,
*aiquant_available_cash
));
}
if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 {
return Ok(false);
}
for symbol in stock_list {
if current_position_symbol
.map(|current| current == symbol)
.unwrap_or(false)
{
continue;
}
let released_exit_position = projected.positions().contains_key(symbol)
&& !slot_working_symbols.contains(symbol)
&& !same_day_sold_symbols.contains(symbol)
&& !pending_full_close_symbols.contains(symbol);
if (projected.positions().contains_key(symbol) && !released_exit_position)
|| same_day_sold_symbols.contains(symbol)
|| (exit_symbols.contains(symbol) && !released_exit_position)
|| pending_full_close_symbols.contains(symbol)
|| delayed_sold_symbols.contains(symbol)
|| intraday_attempted_buys.contains(symbol)
{
continue;
}
if !defer_execution_risk
&& self
.buy_rejection_reason(
ctx,
execution_date,
symbol,
&self.stock_state(ctx, execution_date, symbol)?,
)?
.is_some()
{
continue;
}
let decision_stock = self.stock_state_with_factor_date(
ctx,
decision_date,
selection_factor_date,
symbol,
)?;
let buy_cash = available_buy_cash * self.buy_scale(ctx, day, &decision_stock)?;
if buy_cash <= 0.0 {
return Ok(false);
}
intraday_attempted_buys.insert(symbol.clone());
let cash_before_buy = projected.cash();
let order_result = self.project_order_value(
ctx,
projected,
projection_date,
symbol,
buy_cash,
projected_execution_state,
);
if order_result.was_submitted() {
order_intents.push(OrderIntent::Value {
symbol: symbol.clone(),
value: buy_cash,
reason: "daily_top_up_buy".to_string(),
});
self.remember_position_entry_date(symbol, signal_date);
let spent = (cash_before_buy - projected.cash()).max(0.0);
*aiquant_available_cash = (*aiquant_available_cash - spent).max(0.0);
slot_working_symbols.insert(symbol.clone());
same_bar_buy_symbols.insert(symbol.clone());
*pending_buy_value += available_buy_cash;
if debug_daily_top_up {
daily_top_up_debug_notes.push(format!(
"daily_top_up_fill date={} symbol={} requested_cash={:.4} filled_qty={} spent={:.4} remaining_cash={:.4}",
execution_date,
symbol,
buy_cash,
order_result.filled_quantity,
spent,
*aiquant_available_cash
));
}
return Ok(true);
}
}
Ok(false)
}
fn effective_slot_count( fn effective_slot_count(
working_symbols: &BTreeSet<String>, working_symbols: &BTreeSet<String>,
slot_blocking_symbols: &BTreeSet<String>, slot_blocking_symbols: &BTreeSet<String>,
@@ -7945,6 +8111,7 @@ impl Strategy for PlatformExprStrategy {
let mut exit_symbols = BTreeSet::new(); let mut exit_symbols = BTreeSet::new();
let mut same_day_sold_symbols = BTreeSet::<String>::new(); let mut same_day_sold_symbols = BTreeSet::<String>::new();
let mut intraday_attempted_buys = BTreeSet::<String>::new(); let mut intraday_attempted_buys = BTreeSet::<String>::new();
let mut same_bar_buy_symbols = BTreeSet::<String>::new();
let mut delayed_sold_symbols = BTreeSet::<String>::new(); let mut delayed_sold_symbols = BTreeSet::<String>::new();
let mut unresolved_stop_loss_symbols = BTreeSet::<String>::new(); let mut unresolved_stop_loss_symbols = BTreeSet::<String>::new();
let mut pending_risk_level_forced_exit_symbols = BTreeSet::<String>::new(); let mut pending_risk_level_forced_exit_symbols = BTreeSet::<String>::new();
@@ -8453,12 +8620,23 @@ impl Strategy for PlatformExprStrategy {
quantity: quantity_delta, quantity: quantity_delta,
reason: "daily_position_target_adjust".to_string(), reason: "daily_position_target_adjust".to_string(),
}); });
if quantity_delta > 0 {
same_bar_buy_symbols.insert(position.symbol.clone());
}
} }
} }
aiquant_available_cash = projected.cash(); aiquant_available_cash = projected.cash();
} }
} }
let daily_top_up_active = self.config.daily_top_up_enabled
&& self.config.rotation_enabled
&& !periodic_rebalance
&& trading_ratio > 0.0
&& selection_limit > 0;
let daily_top_up_target_budget = aiquant_total_value * trading_ratio;
let mut daily_top_up_pending_buy_value = 0.0_f64;
for position in ctx.portfolio.positions().values() { for position in ctx.portfolio.positions().values() {
if delayed_sold_symbols.contains(&position.symbol) if delayed_sold_symbols.contains(&position.symbol)
|| pending_full_close_symbols.contains(&position.symbol) || pending_full_close_symbols.contains(&position.symbol)
@@ -8518,6 +8696,36 @@ impl Strategy for PlatformExprStrategy {
} else { } else {
unresolved_stop_loss_symbols.insert(position.symbol.clone()); unresolved_stop_loss_symbols.insert(position.symbol.clone());
} }
if daily_top_up_active {
self.try_daily_top_up_once(
ctx,
&day,
&stock_list,
decision_date,
execution_date,
projection_date,
selection_factor_date,
signal_date,
daily_top_up_target_budget,
selection_limit,
defer_execution_risk,
Some(position.symbol.as_str()),
&mut projected,
&mut projected_execution_state,
&mut order_intents,
&mut aiquant_available_cash,
&mut slot_working_symbols,
&mut same_bar_buy_symbols,
&pending_full_close_symbols,
&same_day_sold_symbols,
&exit_symbols,
&delayed_sold_symbols,
&mut intraday_attempted_buys,
&mut daily_top_up_pending_buy_value,
debug_daily_top_up,
&mut daily_top_up_debug_notes,
)?;
}
continue; continue;
} }
@@ -8553,150 +8761,35 @@ impl Strategy for PlatformExprStrategy {
} }
} }
} }
} if daily_top_up_active {
if self.config.daily_top_up_enabled self.try_daily_top_up_once(
&& self.config.rotation_enabled
&& !periodic_rebalance
&& trading_ratio > 0.0
&& selection_limit > 0
{
let target_budget = aiquant_total_value * trading_ratio;
let mut working_symbols = slot_working_symbols.clone();
if self.config.release_slot_on_exit_signal {
for symbol in &same_day_sold_symbols {
working_symbols.remove(symbol);
slot_working_symbols.remove(symbol);
}
}
let value_symbols = working_symbols.clone();
let mut pending_buy_value = 0.0_f64;
loop {
if Self::effective_slot_count(&working_symbols, &pending_full_close_symbols)
>= selection_limit
{
break;
}
let active_value = if debug_daily_top_up {
self.remaining_buy_cash_active_value(
ctx,
&projected,
projection_date,
&working_symbols,
&value_symbols,
)
} else {
0.0
};
let slot_buy_cash = self.remaining_buy_cash_per_slot(
ctx, ctx,
&projected, &day,
&stock_list,
decision_date,
execution_date,
projection_date, projection_date,
target_budget, selection_factor_date,
signal_date,
daily_top_up_target_budget,
selection_limit, selection_limit,
&working_symbols, defer_execution_risk,
&value_symbols, Some(position.symbol.as_str()),
pending_buy_value, &mut projected,
&mut projected_execution_state,
&mut order_intents,
&mut aiquant_available_cash,
&mut slot_working_symbols,
&mut same_bar_buy_symbols,
&pending_full_close_symbols, &pending_full_close_symbols,
); &same_day_sold_symbols,
let available_buy_cash = slot_buy_cash.min(aiquant_available_cash); &exit_symbols,
if debug_daily_top_up { &delayed_sold_symbols,
daily_top_up_debug_notes.push(format!( &mut intraday_attempted_buys,
"daily_top_up_budget date={} working={} value_symbols={} active_value={:.4} pending_buy_value={:.4} target_budget={:.4} slot_buy_cash={:.4} available_cash={:.4} portfolio_cash={:.4}", &mut daily_top_up_pending_buy_value,
execution_date, debug_daily_top_up,
working_symbols.len(), &mut daily_top_up_debug_notes,
value_symbols.len(), )?;
active_value,
pending_buy_value,
target_budget,
slot_buy_cash,
available_buy_cash,
aiquant_available_cash
));
}
if slot_buy_cash <= 0.0 || available_buy_cash < slot_buy_cash * 0.5 {
break;
}
let mut attempted_any = false;
let mut filled_any = false;
for symbol in &stock_list {
let released_exit_position = projected.positions().contains_key(symbol)
&& !working_symbols.contains(symbol)
&& !same_day_sold_symbols.contains(symbol)
&& !pending_full_close_symbols.contains(symbol);
if (projected.positions().contains_key(symbol) && !released_exit_position)
|| same_day_sold_symbols.contains(symbol)
|| (exit_symbols.contains(symbol) && !released_exit_position)
|| pending_full_close_symbols.contains(symbol)
|| delayed_sold_symbols.contains(symbol)
|| intraday_attempted_buys.contains(symbol)
{
continue;
}
if !defer_execution_risk
&& self
.buy_rejection_reason(
ctx,
execution_date,
symbol,
&self.stock_state(ctx, execution_date, symbol)?,
)?
.is_some()
{
continue;
}
let decision_stock = self.stock_state_with_factor_date(
ctx,
decision_date,
selection_factor_date,
symbol,
)?;
let buy_cash =
available_buy_cash * self.buy_scale(ctx, &day, &decision_stock)?;
if buy_cash <= 0.0 {
break;
}
intraday_attempted_buys.insert(symbol.clone());
attempted_any = true;
let cash_before_buy = projected.cash();
let order_result = self.project_order_value(
ctx,
&mut projected,
projection_date,
symbol,
buy_cash,
&mut projected_execution_state,
);
if order_result.was_submitted() {
order_intents.push(OrderIntent::Value {
symbol: symbol.clone(),
value: buy_cash,
reason: "daily_top_up_buy".to_string(),
});
self.remember_position_entry_date(symbol, signal_date);
let spent = (cash_before_buy - projected.cash()).max(0.0);
aiquant_available_cash = (aiquant_available_cash - spent).max(0.0);
working_symbols.insert(symbol.clone());
slot_working_symbols.insert(symbol.clone());
pending_buy_value += available_buy_cash;
if debug_daily_top_up {
daily_top_up_debug_notes.push(format!(
"daily_top_up_fill date={} symbol={} requested_cash={:.4} filled_qty={} spent={:.4} remaining_cash={:.4}",
execution_date,
symbol,
buy_cash,
order_result.filled_quantity,
spent,
aiquant_available_cash
));
}
filled_any = true;
break;
}
}
if !attempted_any || !filled_any {
break;
}
} }
} }
@@ -21943,7 +22036,7 @@ mod tests {
} }
#[test] #[test]
fn platform_daily_top_up_can_rebuy_released_unsellable_stop_loss_symbol() { fn platform_daily_top_up_skips_current_stop_loss_symbol_like_aiquant() {
let prev_date = d(2026, 3, 31); let prev_date = d(2026, 3, 31);
let date = d(2026, 4, 1); let date = d(2026, 4, 1);
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
@@ -22086,7 +22179,7 @@ mod tests {
decision.order_intents decision.order_intents
); );
assert!( assert!(
decision.order_intents.iter().any(|intent| matches!( !decision.order_intents.iter().any(|intent| matches!(
intent, intent,
OrderIntent::Value { symbol, reason, .. } OrderIntent::Value { symbol, reason, .. }
if symbol == "000001.SZ" && reason == "daily_top_up_buy" if symbol == "000001.SZ" && reason == "daily_top_up_buy"
@@ -22094,6 +22187,15 @@ mod tests {
"{:?}", "{:?}",
decision.order_intents decision.order_intents
); );
assert!(
decision.order_intents.iter().any(|intent| matches!(
intent,
OrderIntent::Value { symbol, reason, .. }
if symbol == "000003.SZ" && reason == "daily_top_up_buy"
)),
"{:?}",
decision.order_intents
);
} }
#[test] #[test]