From 825de1d8862b96d4a9cac6bf8b9651373497b166 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 9 Jul 2026 20:27:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A6=81=E6=AD=A2=E7=9B=AE=E6=A0=87=E7=BB=84?= =?UTF-8?q?=E5=90=88=E8=B0=83=E4=BB=93=E6=94=BE=E5=A4=A7=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E6=9D=83=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/broker.rs | 2 +- crates/fidc-core/tests/explicit_order_flow.rs | 173 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index bf00299..18156b1 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -2162,7 +2162,7 @@ where let mut best_targets = targets.clone(); let mut best_proportion_diff = f64::INFINITY; - let initial_safety = if target_weight_sum > 0.95 { 1.2 } else { 1.0 }; + let initial_safety = 1.0; let mut safety = initial_safety; loop { let mut candidate_targets = targets.clone(); diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index 1f85e2b..dacde8c 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -3725,6 +3725,179 @@ fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() { ); } +#[test] +fn rebalance_optimizer_does_not_scale_targets_above_requested_weight() { + let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); + let data = DataSet::from_components( + vec![ + Instrument { + symbol: "000001.SZ".to_string(), + name: "TargetA".to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: None, + delisted_at: None, + status: "active".to_string(), + }, + Instrument { + symbol: "000002.SZ".to_string(), + name: "TargetB".to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: None, + delisted_at: None, + status: "active".to_string(), + }, + ], + vec![ + DailyMarketSnapshot { + date, + symbol: "000001.SZ".to_string(), + timestamp: Some("2024-01-10 10:18:00".to_string()), + day_open: 82.0, + open: 82.0, + high: 83.0, + low: 81.0, + close: 82.0, + last_price: 82.0, + bid1: 81.99, + ask1: 82.01, + prev_close: 82.0, + volume: 100_000, + minute_volume: 100_000, + bid1_volume: 80_000, + ask1_volume: 80_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 90.2, + lower_limit: 73.8, + price_tick: 0.01, + }, + DailyMarketSnapshot { + date, + symbol: "000002.SZ".to_string(), + timestamp: Some("2024-01-10 10:18:00".to_string()), + day_open: 82.0, + open: 82.0, + high: 83.0, + low: 81.0, + close: 82.0, + last_price: 82.0, + bid1: 81.99, + ask1: 82.01, + prev_close: 82.0, + volume: 100_000, + minute_volume: 100_000, + bid1_volume: 80_000, + ask1_volume: 80_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 90.2, + lower_limit: 73.8, + price_tick: 0.01, + }, + ], + vec![ + DailyFactorSnapshot { + date, + symbol: "000001.SZ".to_string(), + market_cap_bn: 50.0, + free_float_cap_bn: 45.0, + pe_ttm: 15.0, + turnover_ratio: Some(2.0), + effective_turnover_ratio: Some(1.8), + extra_factors: BTreeMap::new(), + }, + DailyFactorSnapshot { + date, + symbol: "000002.SZ".to_string(), + market_cap_bn: 60.0, + free_float_cap_bn: 50.0, + pe_ttm: 18.0, + turnover_ratio: Some(2.0), + effective_turnover_ratio: Some(1.8), + extra_factors: BTreeMap::new(), + }, + ], + vec![ + CandidateEligibility { + date, + symbol: "000001.SZ".to_string(), + is_st: false, + is_star_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: None, + }, + CandidateEligibility { + date, + symbol: "000002.SZ".to_string(), + is_st: false, + is_star_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: None, + }, + ], + vec![BenchmarkSnapshot { + date, + benchmark: "000300.SH".to_string(), + open: 100.0, + close: 100.0, + prev_close: 99.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let mut portfolio = PortfolioState::new(100_000.0); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Open, + ); + + broker + .execute( + date, + &mut portfolio, + &data, + &StrategyDecision { + rebalance: true, + target_weights: BTreeMap::from([ + ("000001.SZ".to_string(), 0.48), + ("000002.SZ".to_string(), 0.48), + ]), + exit_symbols: BTreeSet::new(), + order_intents: Vec::new(), + notes: Vec::new(), + diagnostics: Vec::new(), + risk_decisions: Vec::new(), + }, + ) + .expect("broker execution"); + + assert_eq!( + portfolio + .position("000001.SZ") + .map(|position| position.quantity), + Some(500) + ); + assert_eq!( + portfolio + .position("000002.SZ") + .map(|position| position.quantity), + Some(500) + ); +} + #[test] fn broker_uses_board_specific_min_quantity_and_step_size_for_buy_sizing() { let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();