修正目标金额零数量虚假订单
This commit is contained in:
@@ -4291,6 +4291,15 @@ where
|
|||||||
execution_cursors,
|
execution_cursors,
|
||||||
*global_execution_cursor,
|
*global_execution_cursor,
|
||||||
);
|
);
|
||||||
|
if requested_qty == 0 {
|
||||||
|
report.diagnostics.push(format!(
|
||||||
|
"value_order_skipped symbol={symbol} side=buy reason=below_executable_quantity value={} minimum_order_quantity={} order_step_size={} intent_reason={reason}",
|
||||||
|
value.abs(),
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
self.process_buy(
|
self.process_buy(
|
||||||
date,
|
date,
|
||||||
portfolio,
|
portfolio,
|
||||||
@@ -4312,11 +4321,22 @@ where
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let price = self.value_sell_sizing_price(date, data, symbol, snapshot);
|
let price = self.value_sell_sizing_price(date, data, symbol, snapshot);
|
||||||
|
let minimum_order_quantity = self.minimum_order_quantity(data, symbol);
|
||||||
|
let order_step_size = self.order_step_size(data, symbol);
|
||||||
let requested_qty = self.round_buy_quantity(
|
let requested_qty = self.round_buy_quantity(
|
||||||
((value.abs()) / price).floor() as u32,
|
((value.abs()) / price).floor() as u32,
|
||||||
self.minimum_order_quantity(data, symbol),
|
minimum_order_quantity,
|
||||||
self.order_step_size(data, symbol),
|
order_step_size,
|
||||||
);
|
);
|
||||||
|
if requested_qty == 0 {
|
||||||
|
report.diagnostics.push(format!(
|
||||||
|
"value_order_skipped symbol={symbol} side=sell reason=below_executable_quantity value={} minimum_order_quantity={} order_step_size={} intent_reason={reason}",
|
||||||
|
value.abs(),
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
self.process_sell(
|
self.process_sell(
|
||||||
date,
|
date,
|
||||||
portfolio,
|
portfolio,
|
||||||
@@ -7495,6 +7515,93 @@ mod tests {
|
|||||||
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 2_000);
|
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 2_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");
|
||||||
|
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(10_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",
|
||||||
|
9_995.0,
|
||||||
|
"sub_lot_target_adjust",
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut None,
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut report,
|
||||||
|
)
|
||||||
|
.expect("target value execution");
|
||||||
|
|
||||||
|
assert!(report.order_events.is_empty());
|
||||||
|
assert!(report.fill_events.is_empty());
|
||||||
|
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 1_000);
|
||||||
|
assert!(report.diagnostics.iter().any(|diagnostic| {
|
||||||
|
diagnostic.contains("value_order_skipped symbol=000001.SZ side=sell")
|
||||||
|
&& diagnostic.contains("reason=below_executable_quantity")
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut buy_portfolio = PortfolioState::new(10_000.0);
|
||||||
|
let mut buy_report = BrokerExecutionReport::default();
|
||||||
|
broker
|
||||||
|
.process_target_value(
|
||||||
|
date,
|
||||||
|
&mut buy_portfolio,
|
||||||
|
&data,
|
||||||
|
"000001.SZ",
|
||||||
|
995.0,
|
||||||
|
"sub_lot_target_open",
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut None,
|
||||||
|
&mut BTreeMap::new(),
|
||||||
|
&mut buy_report,
|
||||||
|
)
|
||||||
|
.expect("target value execution");
|
||||||
|
|
||||||
|
assert!(buy_report.order_events.is_empty());
|
||||||
|
assert!(buy_report.fill_events.is_empty());
|
||||||
|
assert!(buy_portfolio.position("000001.SZ").is_none());
|
||||||
|
assert!(buy_report.diagnostics.iter().any(|diagnostic| {
|
||||||
|
diagnostic.contains("value_order_skipped symbol=000001.SZ side=buy")
|
||||||
|
&& diagnostic.contains("reason=below_executable_quantity")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn target_percent_reduction_from_full_to_twenty_percent_sells_eighty_percent() {
|
fn target_percent_reduction_from_full_to_twenty_percent_sells_eighty_percent() {
|
||||||
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