对齐ALV日内补仓执行顺序
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user