对齐ALV日内补仓执行顺序
This commit is contained in:
@@ -2240,6 +2240,172 @@ impl PlatformExprStrategy {
|
||||
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(
|
||||
working_symbols: &BTreeSet<String>,
|
||||
slot_blocking_symbols: &BTreeSet<String>,
|
||||
@@ -7945,6 +8111,7 @@ impl Strategy for PlatformExprStrategy {
|
||||
let mut exit_symbols = BTreeSet::new();
|
||||
let mut same_day_sold_symbols = 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 unresolved_stop_loss_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,
|
||||
reason: "daily_position_target_adjust".to_string(),
|
||||
});
|
||||
if quantity_delta > 0 {
|
||||
same_bar_buy_symbols.insert(position.symbol.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
if delayed_sold_symbols.contains(&position.symbol)
|
||||
|| pending_full_close_symbols.contains(&position.symbol)
|
||||
@@ -8518,6 +8696,36 @@ impl Strategy for PlatformExprStrategy {
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8553,150 +8761,35 @@ impl Strategy for PlatformExprStrategy {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.config.daily_top_up_enabled
|
||||
&& 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(
|
||||
if daily_top_up_active {
|
||||
self.try_daily_top_up_once(
|
||||
ctx,
|
||||
&projected,
|
||||
&day,
|
||||
&stock_list,
|
||||
decision_date,
|
||||
execution_date,
|
||||
projection_date,
|
||||
target_budget,
|
||||
selection_factor_date,
|
||||
signal_date,
|
||||
daily_top_up_target_budget,
|
||||
selection_limit,
|
||||
&working_symbols,
|
||||
&value_symbols,
|
||||
pending_buy_value,
|
||||
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,
|
||||
);
|
||||
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={} active_value={:.4} pending_buy_value={:.4} target_budget={:.4} slot_buy_cash={:.4} available_cash={:.4} portfolio_cash={:.4}",
|
||||
execution_date,
|
||||
working_symbols.len(),
|
||||
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;
|
||||
}
|
||||
&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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21943,7 +22036,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 date = d(2026, 4, 1);
|
||||
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
|
||||
@@ -22086,7 +22179,7 @@ mod tests {
|
||||
decision.order_intents
|
||||
);
|
||||
assert!(
|
||||
decision.order_intents.iter().any(|intent| matches!(
|
||||
!decision.order_intents.iter().any(|intent| matches!(
|
||||
intent,
|
||||
OrderIntent::Value { symbol, reason, .. }
|
||||
if symbol == "000001.SZ" && reason == "daily_top_up_buy"
|
||||
@@ -22094,6 +22187,15 @@ mod tests {
|
||||
"{:?}",
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user