修正AiQuant目标组合执行风控延后语义
This commit is contained in:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user