对齐ALV补仓订单返回语义

This commit is contained in:
boris
2026-07-08 00:59:11 +08:00
parent 749b5e3b9c
commit 12da5a4704
+63 -34
View File
@@ -357,6 +357,36 @@ struct ProjectedExecutionFill {
next_cursor: NaiveDateTime,
}
#[derive(Debug, Clone, Copy, Default)]
struct ProjectedOrderValueResult {
submitted_quantity: u32,
filled_quantity: u32,
}
impl ProjectedOrderValueResult {
fn not_submitted() -> Self {
Self::default()
}
fn submitted_without_fill(submitted_quantity: u32) -> Self {
Self {
submitted_quantity,
filled_quantity: 0,
}
}
fn submitted_with_fill(submitted_quantity: u32, filled_quantity: u32) -> Self {
Self {
submitted_quantity,
filled_quantity,
}
}
fn was_submitted(&self) -> bool {
self.submitted_quantity > 0
}
}
#[derive(Debug, Clone)]
struct DayExpressionState {
date: NaiveDate,
@@ -2037,9 +2067,9 @@ impl PlatformExprStrategy {
return None;
}
if cash_delta > 0.0 {
let filled =
let result =
self.project_order_value(ctx, projected, date, symbol, cash_delta, execution_state);
return (filled > 0).then_some(filled);
return (result.filled_quantity > 0).then_some(result.filled_quantity);
}
if !Self::defer_projection_execution_risk(ctx, date)
@@ -2235,23 +2265,23 @@ impl PlatformExprStrategy {
symbol: &str,
order_value: f64,
execution_state: &mut ProjectedExecutionState,
) -> u32 {
) -> ProjectedOrderValueResult {
if order_value <= 0.0 {
return 0;
return ProjectedOrderValueResult::not_submitted();
}
let round_lot = self.projected_round_lot(ctx, symbol);
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
let order_step_size = self.projected_order_step_size(ctx, symbol);
let market = match ctx.data.market(date, symbol) {
Some(market) => market,
None => return 0,
None => return ProjectedOrderValueResult::not_submitted(),
};
let stock = match self.stock_state(ctx, date, symbol) {
Ok(stock) => stock,
Err(BacktestError::Data(crate::data::DataSetError::MissingSnapshot { .. })) => {
return 0;
return ProjectedOrderValueResult::not_submitted();
}
Err(_) => return 0,
Err(_) => return ProjectedOrderValueResult::not_submitted(),
};
if !Self::defer_projection_execution_risk(ctx, date)
&& self
@@ -2260,7 +2290,7 @@ impl PlatformExprStrategy {
.flatten()
.is_some()
{
return 0;
return ProjectedOrderValueResult::not_submitted();
}
let raw_sizing_price = if self.config.aiquant_transaction_cost {
self.aiquant_scheduled_last_price(ctx, date, symbol)
@@ -2274,7 +2304,7 @@ impl PlatformExprStrategy {
raw_sizing_price
};
if !sizing_price.is_finite() || sizing_price <= 0.0 {
return 0;
return ProjectedOrderValueResult::not_submitted();
}
let snapshot_requested_qty = self.value_buy_quantity(
projected.cash().min(order_value),
@@ -2304,8 +2334,9 @@ impl PlatformExprStrategy {
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
}
if quantity == 0 {
return 0;
return ProjectedOrderValueResult::not_submitted();
}
let submitted_quantity = quantity;
let defer_projection_execution_risk = Self::defer_projection_execution_risk(ctx, date);
let fill = self
.projected_select_execution_fill(
@@ -2363,12 +2394,12 @@ impl PlatformExprStrategy {
}
});
let Some(fill) = fill else {
return 0;
return ProjectedOrderValueResult::submitted_without_fill(submitted_quantity);
};
let gross_amount = fill.price * fill.quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out > cash_limit + 1e-6 {
return 0;
return ProjectedOrderValueResult::submitted_without_fill(submitted_quantity);
}
projected.apply_cash_delta(-cash_out);
projected
@@ -2381,7 +2412,7 @@ impl PlatformExprStrategy {
execution_state
.execution_cursors
.insert(symbol.to_string(), fill.next_cursor);
fill.quantity
ProjectedOrderValueResult::submitted_with_fill(submitted_quantity, fill.quantity)
}
fn defer_projection_execution_risk(ctx: &StrategyContext<'_>, date: NaiveDate) -> bool {
@@ -8600,7 +8631,7 @@ impl Strategy for PlatformExprStrategy {
intraday_attempted_buys.insert(symbol.clone());
attempted_any = true;
let cash_before_buy = projected.cash();
let filled_qty = self.project_order_value(
let order_result = self.project_order_value(
ctx,
&mut projected,
projection_date,
@@ -8608,15 +8639,13 @@ impl Strategy for PlatformExprStrategy {
buy_cash,
&mut projected_execution_state,
);
if filled_qty > 0 {
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);
}
if filled_qty > 0 {
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());
@@ -8628,7 +8657,7 @@ impl Strategy for PlatformExprStrategy {
execution_date,
symbol,
buy_cash,
filled_qty,
order_result.filled_quantity,
spent,
aiquant_available_cash
));
@@ -8759,7 +8788,7 @@ impl Strategy for PlatformExprStrategy {
continue;
}
let cash_before_buy = projected.cash();
let filled_qty = self.project_order_value(
let order_result = self.project_order_value(
ctx,
&mut projected,
projection_date,
@@ -8767,7 +8796,7 @@ impl Strategy for PlatformExprStrategy {
buy_cash,
&mut projected_execution_state,
);
if filled_qty > 0 {
if order_result.was_submitted() {
order_intents.push(OrderIntent::Value {
symbol: symbol.clone(),
value: buy_cash,
@@ -8855,7 +8884,7 @@ impl Strategy for PlatformExprStrategy {
continue;
}
let cash_before_buy = projected.cash();
let filled_qty = self.project_order_value(
let order_result = self.project_order_value(
ctx,
&mut projected,
projection_date,
@@ -8863,7 +8892,7 @@ impl Strategy for PlatformExprStrategy {
buy_cash,
&mut projected_execution_state,
);
if filled_qty > 0 {
if order_result.was_submitted() {
order_intents.push(OrderIntent::Value {
symbol: symbol.clone(),
value: buy_cash,
@@ -10790,7 +10819,7 @@ mod tests {
let mut projected = portfolio.clone();
let mut execution_state = super::ProjectedExecutionState::default();
let filled = strategy.project_order_value(
let result = strategy.project_order_value(
&ctx,
&mut projected,
date,
@@ -10799,7 +10828,7 @@ mod tests {
&mut execution_state,
);
assert_eq!(filled, 24_400);
assert_eq!(result.filled_quantity, 24_400);
let position = projected.position(symbol).expect("position");
assert_eq!(position.quantity, 24_400);
assert!((position.average_cost - 5.12022).abs() < 1e-9);
@@ -10924,7 +10953,7 @@ mod tests {
let mut projected = portfolio.clone();
let mut execution_state = super::ProjectedExecutionState::default();
let filled = strategy.project_order_value(
let result = strategy.project_order_value(
&ctx,
&mut projected,
date,
@@ -10933,7 +10962,7 @@ mod tests {
&mut execution_state,
);
assert_eq!(filled, 24_400);
assert_eq!(result.filled_quantity, 24_400);
}
#[test]
@@ -19445,7 +19474,7 @@ mod tests {
}
#[test]
fn platform_daily_top_up_continues_after_unfilled_candidate() {
fn platform_daily_top_up_counts_submitted_zero_fill_candidate_as_strategy_slot() {
let prev_date = d(2025, 2, 2);
let date = d(2025, 2, 3);
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
@@ -19590,7 +19619,7 @@ mod tests {
symbol,
reason,
..
} if symbol == "000002.SZ" && reason == "daily_top_up_buy"
} if symbol == "000003.SZ" && reason == "daily_top_up_buy"
)),
"{:?}",
decision.order_intents
@@ -19598,7 +19627,7 @@ mod tests {
assert!(
!decision.order_intents.iter().any(|intent| matches!(
intent,
OrderIntent::Value { symbol, .. } if symbol == "000003.SZ"
OrderIntent::Value { symbol, .. } if symbol == "000002.SZ"
)),
"{:?}",
decision.order_intents
@@ -23409,7 +23438,7 @@ mod tests {
assert!(ctx.data.execution_quotes_on(date, symbol).is_empty());
assert!(!ctx.data.execution_quotes_on(date, other_symbol).is_empty());
let filled = strategy.project_order_value(
let result = strategy.project_order_value(
&ctx,
&mut projected,
date,
@@ -23418,7 +23447,7 @@ mod tests {
&mut execution_state,
);
assert_eq!(filled, 0);
assert_eq!(result.filled_quantity, 0);
assert_eq!(
projected.position(symbol).map(|position| position.quantity),
None
@@ -23560,7 +23589,7 @@ mod tests {
.execution_quotes_on(decision_date, other_symbol)
.is_empty()
);
let filled = strategy.project_order_value(
let result = strategy.project_order_value(
&ctx,
&mut projected,
decision_date,
@@ -23569,10 +23598,10 @@ mod tests {
&mut execution_state,
);
assert!(filled > 0);
assert!(result.filled_quantity > 0);
assert_eq!(
projected.position(symbol).map(|position| position.quantity),
Some(filled)
Some(result.filled_quantity)
);
assert!(projected.cash() < portfolio.cash());
}