修正无变化目标订单记录
This commit is contained in:
@@ -3897,19 +3897,6 @@ where
|
|||||||
|
|
||||||
if target_value <= f64::EPSILON {
|
if target_value <= f64::EPSILON {
|
||||||
if current_qty == 0 {
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if data.market(date, symbol).is_none() {
|
if data.market(date, symbol).is_none() {
|
||||||
@@ -3980,24 +3967,6 @@ where
|
|||||||
commission_state,
|
commission_state,
|
||||||
report,
|
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(())
|
Ok(())
|
||||||
@@ -4021,6 +3990,13 @@ where
|
|||||||
commission_state: &mut BTreeMap<u64, f64>,
|
commission_state: &mut BTreeMap<u64, f64>,
|
||||||
report: &mut BrokerExecutionReport,
|
report: &mut BrokerExecutionReport,
|
||||||
) -> Result<(), BacktestError> {
|
) -> 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
|
let snapshot = data
|
||||||
.market(date, symbol)
|
.market(date, symbol)
|
||||||
.ok_or_else(|| BacktestError::MissingPrice {
|
.ok_or_else(|| BacktestError::MissingPrice {
|
||||||
@@ -4028,10 +4004,6 @@ where
|
|||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
field: price_field_name(self.execution_price_field),
|
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 {
|
let algo_request = AlgoExecutionRequest {
|
||||||
style: match style {
|
style: match style {
|
||||||
AlgoOrderStyle::Vwap => AlgoExecutionStyle::Vwap,
|
AlgoOrderStyle::Vwap => AlgoExecutionStyle::Vwap,
|
||||||
@@ -4042,22 +4014,6 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
if target_value <= f64::EPSILON {
|
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(
|
self.process_sell(
|
||||||
date,
|
date,
|
||||||
portfolio,
|
portfolio,
|
||||||
@@ -4099,24 +4055,6 @@ where
|
|||||||
commission_state,
|
commission_state,
|
||||||
report,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -4198,24 +4136,6 @@ where
|
|||||||
report,
|
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(())
|
Ok(())
|
||||||
@@ -7807,6 +7727,96 @@ mod tests {
|
|||||||
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 2_000);
|
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]
|
#[test]
|
||||||
fn target_value_delta_below_order_step_is_audited_without_creating_zero_quantity_order() {
|
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");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
|||||||
Reference in New Issue
Block a user