修正AiQuant目标组合执行风控延后语义

This commit is contained in:
boris
2026-07-10 12:01:11 +08:00
parent 1272e427a1
commit 56859dbe32
+111 -1
View File
@@ -2720,6 +2720,12 @@ where
let Some(position) = portfolio.position(symbol) else {
return 0;
};
if self.aiquant_execution_rules {
let sellable = position
.sellable_qty(date)
.saturating_sub(self.reserved_open_sell_quantity(symbol, None));
return current_qty.saturating_sub(sellable.min(current_qty));
}
let Ok(snapshot) = data.require_market(date, symbol) else {
return current_qty;
};
@@ -2767,6 +2773,9 @@ where
minimum_order_quantity: u32,
order_step_size: u32,
) -> u32 {
if self.aiquant_execution_rules {
return u32::MAX;
}
let Ok(snapshot) = data.require_market(date, symbol) else {
return current_qty;
};
@@ -7250,6 +7259,105 @@ mod tests {
);
}
#[test]
fn aiquant_target_portfolio_smart_defers_buy_risk_during_target_sizing() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![limit_test_snapshot()],
Vec::new(),
vec![limit_test_candidate(false, true)],
vec![limit_test_benchmark()],
Vec::new(),
Vec::new(),
)
.expect("valid dataset");
let portfolio = PortfolioState::new(1_000_000.0);
let mut target_weights = BTreeMap::new();
target_weights.insert("000001.SZ".to_string(), 0.50);
let default_broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_volume_limit(false)
.with_liquidity_limit(false);
let (default_targets, _) = default_broker
.target_quantities(date, &portfolio, &data, &target_weights)
.expect("default target quantities");
assert_eq!(default_targets.get("000001.SZ").copied().unwrap_or(0), 0);
let aiquant_broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_aiquant_execution_rules(true)
.with_volume_limit(false)
.with_liquidity_limit(false);
let (aiquant_targets, _) = aiquant_broker
.target_quantities(date, &portfolio, &data, &target_weights)
.expect("aiquant target quantities");
assert_eq!(aiquant_targets.get("000001.SZ").copied(), Some(50_000));
}
#[test]
fn aiquant_target_portfolio_smart_rejects_deferred_buy_risk_at_order_stage() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![limit_test_snapshot()],
Vec::new(),
vec![limit_test_candidate(false, true)],
vec![limit_test_benchmark()],
Vec::new(),
Vec::new(),
)
.expect("valid dataset");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_aiquant_execution_rules(true)
.with_volume_limit(false)
.with_liquidity_limit(false);
let mut portfolio = PortfolioState::new(1_000_000.0);
let mut target_weights = BTreeMap::new();
target_weights.insert("000001.SZ".to_string(), 0.50);
let mut report = BrokerExecutionReport::default();
broker
.process_target_portfolio_smart(
date,
&mut portfolio,
&data,
&target_weights,
None,
None,
"aiquant_deferred_buy_risk",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("deferred buy risk is handled at order stage");
assert!(portfolio.position("000001.SZ").is_none());
assert!(report.fill_events.is_empty());
let rejected = report
.order_events
.iter()
.find(|event| event.symbol == "000001.SZ")
.expect("buy-disabled target should be rejected at order stage");
assert_eq!(rejected.side, OrderSide::Buy);
assert_ne!(rejected.status, OrderStatus::Filled);
assert_eq!(rejected.filled_quantity, 0);
assert!(rejected.reason.contains("buy_disabled"), "{rejected:?}");
}
#[test]
fn target_portfolio_smart_scales_buys_when_full_targets_exceed_cash_by_fees() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
@@ -7312,7 +7420,9 @@ mod tests {
assert_eq!(target_quantities.get("000001.SZ").copied(), Some(400));
assert_eq!(target_quantities.get("000002.SZ").copied(), Some(400));
assert!(
diagnostics.iter().any(|line| line.contains("rebalance_safety_scaled")),
diagnostics
.iter()
.any(|line| line.contains("rebalance_safety_scaled")),
"{diagnostics:?}"
);
assert!(