From 56859dbe32743a1860541c026a38878d0645b4fe Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 10 Jul 2026 12:01:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3AiQuant=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E7=BB=84=E5=90=88=E6=89=A7=E8=A1=8C=E9=A3=8E=E6=8E=A7=E5=BB=B6?= =?UTF-8?q?=E5=90=8E=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/broker.rs | 112 ++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index affa73e..a901359 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -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!(