修正无变化目标订单记录

This commit is contained in:
boris
2026-08-01 22:10:44 +08:00
parent 839ca1fa0d
commit fe7e0f397f
+97 -87
View File
@@ -3897,19 +3897,6 @@ where
if target_value <= f64::EPSILON {
if current_qty == 0 {
report.order_events.push(OrderEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: None,
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target value"),
});
return Ok(());
}
if data.market(date, symbol).is_none() {
@@ -3980,24 +3967,6 @@ where
commission_state,
report,
)?;
} else {
report.order_events.push(OrderEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: None,
symbol: symbol.to_string(),
side: if current_qty > 0 {
OrderSide::Sell
} else {
OrderSide::Buy
},
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target value"),
});
}
Ok(())
@@ -4021,6 +3990,13 @@ where
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let current_qty = portfolio
.position(symbol)
.map(|pos| pos.quantity)
.unwrap_or(0);
if target_value <= f64::EPSILON && current_qty == 0 {
return Ok(());
}
let snapshot = data
.market(date, symbol)
.ok_or_else(|| BacktestError::MissingPrice {
@@ -4028,10 +4004,6 @@ where
symbol: symbol.to_string(),
field: price_field_name(self.execution_price_field),
})?;
let current_qty = portfolio
.position(symbol)
.map(|pos| pos.quantity)
.unwrap_or(0);
let algo_request = AlgoExecutionRequest {
style: match style {
AlgoOrderStyle::Vwap => AlgoExecutionStyle::Vwap,
@@ -4042,22 +4014,6 @@ where
};
if target_value <= f64::EPSILON {
if current_qty == 0 {
report.order_events.push(OrderEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: None,
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target value"),
});
return Ok(());
}
self.process_sell(
date,
portfolio,
@@ -4099,24 +4055,6 @@ where
commission_state,
report,
)?;
} else {
report.order_events.push(OrderEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: None,
symbol: symbol.to_string(),
side: if current_qty > 0 {
OrderSide::Sell
} else {
OrderSide::Buy
},
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target value"),
});
}
Ok(())
}
@@ -4198,24 +4136,6 @@ where
report,
)?;
}
} else {
report.order_events.push(OrderEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: None,
symbol: symbol.to_string(),
side: if current_qty > 0 {
OrderSide::Sell
} else {
OrderSide::Buy
},
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target shares"),
});
}
Ok(())
@@ -7807,6 +7727,96 @@ mod tests {
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 2_000);
}
#[test]
fn unchanged_targets_do_not_create_zero_quantity_orders() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_matching_type(MatchingType::NextBarOpen)
.with_volume_limit(false)
.with_liquidity_limit(false)
.with_inactive_limit(false);
let mut snapshot = limit_test_snapshot();
snapshot.date = date;
snapshot.prev_close = 10.0;
snapshot.open = 10.0;
snapshot.close = 10.0;
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot],
Vec::new(),
vec![limit_test_candidate(true, true)],
vec![limit_test_benchmark()],
Vec::new(),
Vec::new(),
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(20_000.0);
portfolio.position_mut("000001.SZ").buy(
date.pred_opt().expect("previous date"),
1_000,
10.0,
);
portfolio.apply_cash_delta(-10_000.0);
let mut report = BrokerExecutionReport::default();
broker
.process_target_value(
date,
&mut portfolio,
&data,
"000001.SZ",
10_000.0,
"unchanged_target_value",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("unchanged target value");
broker
.process_timed_target_value(
date,
&mut portfolio,
&data,
"000001.SZ",
10_000.0,
AlgoOrderStyle::Twap,
None,
None,
"unchanged timed target value",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("unchanged timed target value");
broker
.process_target_shares(
date,
&mut portfolio,
&data,
"000001.SZ",
1_000,
"unchanged target shares",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("unchanged target shares");
assert!(report.order_events.is_empty());
assert!(report.fill_events.is_empty());
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 1_000);
}
#[test]
fn target_value_delta_below_order_step_is_audited_without_creating_zero_quantity_order() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");