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