From 5f5f0fcf164aae649eb9b5e6fada8415f849a0c3 Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 12 Jul 2026 16:44:39 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=9B=AE=E6=A0=87=E4=BB=93?= =?UTF-8?q?=E4=BD=8D=E9=99=8D=E4=BB=93=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/broker.rs | 116 +++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index ad6ceff..326d333 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -7437,6 +7437,122 @@ mod tests { assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 500); } + #[test] + fn target_value_reduction_from_full_to_twenty_percent_sells_eighty_percent() { + 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(100_000.0); + portfolio.position_mut("000001.SZ").buy( + date.pred_opt().expect("previous date"), + 10_000, + 10.0, + ); + portfolio.apply_cash_delta(-100_000.0); + let mut report = BrokerExecutionReport::default(); + + broker + .process_target_value( + date, + &mut portfolio, + &data, + "000001.SZ", + 20_000.0, + "target_value_20_percent", + &mut BTreeMap::new(), + &mut BTreeMap::new(), + &mut None, + &mut BTreeMap::new(), + &mut report, + ) + .expect("target value reduction"); + + assert_eq!(report.fill_events.len(), 1); + assert_eq!(report.fill_events[0].side, OrderSide::Sell); + assert_eq!(report.fill_events[0].quantity, 8_000); + assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 2_000); + } + + #[test] + 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 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(100_000.0); + portfolio.position_mut("000001.SZ").buy( + date.pred_opt().expect("previous date"), + 10_000, + 10.0, + ); + portfolio.apply_cash_delta(-100_000.0); + let mut report = BrokerExecutionReport::default(); + + broker + .process_target_percent( + date, + &mut portfolio, + &data, + "000001.SZ", + 0.2, + "target_percent_20_percent", + &mut BTreeMap::new(), + &mut BTreeMap::new(), + &mut None, + &mut BTreeMap::new(), + &mut report, + ) + .expect("target percent reduction"); + + assert_eq!(report.fill_events.len(), 1); + assert_eq!(report.fill_events[0].side, OrderSide::Sell); + assert_eq!(report.fill_events[0].quantity, 8_000); + assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 2_000); + } + #[test] fn target_portfolio_smart_ignores_zero_weight_symbols_without_market_snapshot() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");