diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 11c9633..856e260 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -380,6 +380,7 @@ pub struct BrokerSimulator { runtime_intraday_start_time: Cell>, runtime_intraday_end_time: Cell>, runtime_decision_date: Cell>, + runtime_buy_denials: RefCell>, runtime_order_created_date: Cell>, runtime_decision_total_equity: Cell>, runtime_target_position_limit: Cell>, @@ -412,6 +413,7 @@ impl BrokerSimulator { runtime_intraday_start_time: Cell::new(None), runtime_intraday_end_time: Cell::new(None), runtime_decision_date: Cell::new(None), + runtime_buy_denials: RefCell::new(BTreeMap::new()), runtime_order_created_date: Cell::new(None), runtime_decision_total_equity: Cell::new(None), runtime_target_position_limit: Cell::new(None), @@ -448,6 +450,7 @@ impl BrokerSimulator { runtime_intraday_start_time: Cell::new(None), runtime_intraday_end_time: Cell::new(None), runtime_decision_date: Cell::new(None), + runtime_buy_denials: RefCell::new(BTreeMap::new()), runtime_order_created_date: Cell::new(None), runtime_decision_total_equity: Cell::new(None), runtime_target_position_limit: Cell::new(None), @@ -1385,6 +1388,7 @@ where decision: &StrategyDecision, ) -> Result { let previous_decision_date = self.runtime_decision_date.get(); + let previous_buy_denials = self.runtime_buy_denials.replace(decision.buy_denials.clone()); let previous_order_created_date = self.runtime_order_created_date.get(); let previous_decision_total_equity = self.runtime_decision_total_equity.get(); self.runtime_decision_date.set(Some(decision_date)); @@ -1393,6 +1397,7 @@ where self.runtime_decision_total_equity .set(decision_total_equity.filter(|equity| equity.is_finite() && *equity >= 0.0)); let result = self.execute_with_runtime_dates(date, portfolio, data, decision); + self.runtime_buy_denials.replace(previous_buy_denials); self.runtime_decision_date.set(previous_decision_date); self.runtime_order_created_date .set(previous_order_created_date); @@ -4189,6 +4194,9 @@ where if !rule.allowed { return rule.reason; } + if let Some(reason) = self.runtime_buy_denials.borrow().get(symbol) { + return Some(reason.clone()); + } match self.market_fillable_quantity( snapshot, OrderSide::Buy, @@ -6140,6 +6148,12 @@ where data.instrument(symbol), algo_request, ); + let rule = if rule.allowed && emit_creation_events { + self.runtime_buy_denials.borrow().get(symbol) + .map_or(rule, |reason| RuleCheck::reject(reason.clone())) + } else { + rule + }; if !rule.allowed { let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string(); let status = match rule.reason.as_deref() { @@ -8204,6 +8218,7 @@ mod tests { fn next_open_buy_decision() -> StrategyDecision { StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: "000001.SZ".to_string(), quantity: 100, @@ -8213,8 +8228,58 @@ mod tests { } } + #[test] + fn decision_buy_denial_blocks_topup_but_allows_sell_and_does_not_leak() { + let first = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(); + let second = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(); + let data = DataSet::from_components( + vec![limit_test_instrument()], + vec![dated_limit_test_snapshot(first), dated_limit_test_snapshot(second)], + Vec::new(), + vec![dated_limit_test_candidate(first, false, false, true, true), + dated_limit_test_candidate(second, false, false, true, true)], + vec![dated_limit_test_benchmark(first), dated_limit_test_benchmark(second)], + ).unwrap(); + let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_matching_type(MatchingType::CurrentBarClose); + let mut portfolio = PortfolioState::new(100_000.0); + broker.execute(first, &mut portfolio, &data, &next_open_buy_decision()).unwrap(); + assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 100); + let mut blocked = StrategyDecision::default(); + blocked.buy_denials.insert("000001.SZ".to_string(), "strategy_buy_condition_false".to_string()); + blocked.order_intents.push(OrderIntent::TargetValue { + symbol: "000001.SZ".to_string(), target_value: 3_000.0, reason: "topup".to_string(), + }); + let report = broker.execute(second, &mut portfolio, &data, &blocked).unwrap(); + assert!(report.fills.is_empty()); + assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 100); + assert!(broker.runtime_buy_denials.borrow().is_empty()); + blocked.order_intents = next_open_sell_decision().order_intents; + let report = broker.execute(second, &mut portfolio, &data, &blocked).unwrap(); + assert_eq!(report.fills.len(), 1); + assert_eq!(report.fills[0].side, OrderSide::Sell); + assert!(broker.runtime_buy_denials.borrow().is_empty()); + } + + #[test] + fn decision_buy_denial_does_not_rewrite_existing_pending_order() { + let date = limit_test_snapshot().date; + let data = DataSet::from_components(vec![limit_test_instrument()], vec![limit_test_snapshot()], + Vec::new(), vec![limit_test_candidate(true, true)], vec![limit_test_benchmark()]).unwrap(); + let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) + .with_matching_type(MatchingType::CurrentBarClose); + broker.upsert_open_order(test_open_order(99)); + let mut decision = StrategyDecision::default(); + decision.buy_denials.insert("000001.SZ".to_string(), "strategy_buy_condition_false".to_string()); + let mut portfolio = PortfolioState::new(100_000.0); + let report = broker.execute(date, &mut portfolio, &data, &decision).unwrap(); + assert!(!report.fills.is_empty()); + assert!(broker.runtime_buy_denials.borrow().is_empty()); + } + fn next_open_sell_decision() -> StrategyDecision { StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: "000001.SZ".to_string(), quantity: -100, @@ -10192,6 +10257,7 @@ mod tests { ) .expect("valid dataset"); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::TargetValue { symbol: "000002.SZ".to_string(), @@ -10304,6 +10370,7 @@ mod tests { .position_mut("000002.SZ") .buy(prev_date, 1_000, 10.0); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::TargetValue { symbol: "000001.SZ".to_string(), @@ -10371,6 +10438,7 @@ mod tests { .position_mut("000002.SZ") .buy(prev_date, 1_000, 10.0); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::TargetValue { symbol: "000003.SZ".to_string(), @@ -10431,6 +10499,7 @@ mod tests { .position_mut("000001.SZ") .buy(prev_date, 1_000, 10.0); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::TargetValue { symbol: "000003.SZ".to_string(), target_value: 9_000.0, @@ -10595,6 +10664,7 @@ mod tests { .with_inactive_limit(false); let mut portfolio = PortfolioState::new(20_000.0); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::TargetValue { symbol: "000001.SZ".to_string(), target_value: 10_000.0, @@ -10636,6 +10706,7 @@ mod tests { .with_inactive_limit(false); let mut portfolio = PortfolioState::new(20_000.0); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::TargetValue { symbol: "000001.SZ".to_string(), target_value: 10_000.0, @@ -10673,6 +10744,7 @@ mod tests { .with_inactive_limit(false); let mut portfolio = PortfolioState::new(20_000.0); let decision = StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::TargetPortfolioSmart { target_weights: BTreeMap::from([("000001.SZ".to_string(), 0.5)]), order_prices: None, diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 5cd3cd1..e0b2053 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -4856,6 +4856,7 @@ mod tests { ) -> Result { if ctx.decision_date == self.decision_date && ctx.portfolio.position(SYMBOL).is_none() { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -4896,6 +4897,7 @@ mod tests { ); if ctx.portfolio.position(SYMBOL).is_none() { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -4955,6 +4957,7 @@ mod tests { rule: &ScheduleRule, ) -> Result { Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -4992,6 +4995,7 @@ mod tests { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -5028,6 +5032,7 @@ mod tests { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::TargetPortfolioSmart { target_weights: self.target_weights.clone(), order_prices: None, @@ -5070,6 +5075,7 @@ mod tests { Vec::new() }; Ok(StrategyDecision { + buy_denials: Default::default(), order_intents, ..StrategyDecision::default() }) @@ -5106,6 +5112,7 @@ mod tests { return Ok(StrategyDecision::default()); }; Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol, quantity: 100, @@ -5166,6 +5173,7 @@ mod tests { .unwrap_or_default() )); Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -5206,6 +5214,7 @@ mod tests { && ctx.portfolio.position(SYMBOL).is_none() { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -5217,6 +5226,7 @@ mod tests { if ctx.decision_date == self.sell_decision_date { if let Some(position) = ctx.portfolio.position(SYMBOL) { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: -(position.quantity as i32), @@ -5256,6 +5266,7 @@ mod tests { && ctx.portfolio.position(SYMBOL).is_none() { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: SYMBOL.to_string(), quantity: 100, @@ -5267,6 +5278,7 @@ mod tests { if ctx.decision_date == self.rebuy_decision_date { if let Some(position) = ctx.portfolio.position(SYMBOL) { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::Shares { symbol: SYMBOL.to_string(), diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 648b3cc..3a04c39 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9913,6 +9913,7 @@ impl PlatformExprStrategy { )]; diagnostics.extend(action_diagnostics); Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -12578,6 +12579,7 @@ impl Strategy for PlatformExprStrategy { )); } return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols, @@ -13854,6 +13856,7 @@ impl Strategy for PlatformExprStrategy { ]; Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols, diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 6744815..68cc98f 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -977,6 +977,7 @@ fn safe_ratio(numerator: f64, denominator: f64) -> f64 { #[derive(Debug, Clone, Default)] pub struct StrategyDecision { + pub buy_denials: BTreeMap, pub rebalance: bool, pub target_weights: BTreeMap, pub exit_symbols: BTreeSet, @@ -988,6 +989,7 @@ pub struct StrategyDecision { impl StrategyDecision { pub fn merge_from(&mut self, mut other: StrategyDecision) { + self.buy_denials.append(&mut other.buy_denials); self.rebalance |= other.rebalance; self.target_weights.append(&mut other.target_weights); self.exit_symbols.append(&mut other.exit_symbols); @@ -998,7 +1000,8 @@ impl StrategyDecision { } pub fn is_empty(&self) -> bool { - !self.rebalance + self.buy_denials.is_empty() + && !self.rebalance && self.target_weights.is_empty() && self.exit_symbols.is_empty() && self.order_intents.is_empty() @@ -1569,6 +1572,7 @@ impl Strategy for CnSmallCapRotationStrategy { if self.config.in_skip_window(ctx.decision_date) { self.last_gross_exposure = Some(0.0); return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: true, target_weights: BTreeMap::new(), exit_symbols: ctx.portfolio.positions().keys().cloned().collect(), @@ -1590,6 +1594,7 @@ impl Strategy for CnSmallCapRotationStrategy { if message.contains("signal series insufficient") => { return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1765,6 +1770,7 @@ impl Strategy for CnSmallCapRotationStrategy { self.last_gross_exposure = Some(gross_exposure); Ok(StrategyDecision { + buy_denials: Default::default(), rebalance, target_weights, exit_symbols, @@ -2773,6 +2779,7 @@ impl Strategy for OmniMicroCapStrategy { let lagged_execution = ctx.is_lagged_execution(); if self.config.in_skip_window(signal_date) { return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: ctx.portfolio.positions().keys().cloned().collect(), @@ -2803,6 +2810,7 @@ impl Strategy for OmniMicroCapStrategy { if message.contains("insufficient benchmark") => { return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -3013,6 +3021,7 @@ impl Strategy for OmniMicroCapStrategy { ]; Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols, diff --git a/crates/fidc-core/tests/corporate_actions.rs b/crates/fidc-core/tests/corporate_actions.rs index f72351d..27ed34a 100644 --- a/crates/fidc-core/tests/corporate_actions.rs +++ b/crates/fidc-core/tests/corporate_actions.rs @@ -90,6 +90,7 @@ impl Strategy for BuyAndHoldStrategy { ctx: &StrategyContext<'_>, ) -> Result { Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), diff --git a/crates/fidc-core/tests/decision_quote_preload.rs b/crates/fidc-core/tests/decision_quote_preload.rs index fc06c99..81f899c 100644 --- a/crates/fidc-core/tests/decision_quote_preload.rs +++ b/crates/fidc-core/tests/decision_quote_preload.rs @@ -37,6 +37,7 @@ impl Strategy for DecisionQuoteReader { self.day_count += 1; if self.day_count == 1 { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Value { symbol: "000001.SZ".to_string(), value: 5_000.0, @@ -626,6 +627,7 @@ impl Strategy for MultiTimeDecisionQuoteReader { self.day_count += 1; if self.day_count == 1 { return Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Value { symbol: "000001.SZ".to_string(), value: 5_000.0, diff --git a/crates/fidc-core/tests/delisting.rs b/crates/fidc-core/tests/delisting.rs index 48e3f4c..96d9bc1 100644 --- a/crates/fidc-core/tests/delisting.rs +++ b/crates/fidc-core/tests/delisting.rs @@ -24,6 +24,7 @@ impl Strategy for BuyThenHoldStrategy { ) -> Result { if ctx.decision_date == d(2025, 1, 2) && ctx.portfolio.position("000001.SZ").is_none() { return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 49cb815..05cbebc 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -295,6 +295,7 @@ impl Strategy for HookProbeStrategy { .borrow_mut() .push(format!("on_day:{}", ctx.execution_date)); Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -334,6 +335,7 @@ impl Strategy for AuctionOrderStrategy { _ctx: &StrategyContext<'_>, ) -> Result { Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -377,6 +379,7 @@ impl Strategy for FuturesOrderStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -413,6 +416,7 @@ impl Strategy for FuturesLimitOrderStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Futures { intent: FuturesOrderIntent::limit_open( "IF2501", @@ -444,6 +448,7 @@ impl Strategy for FuturesInvalidTickLimitStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Futures { intent: FuturesOrderIntent::limit_open( "IF2501", @@ -475,6 +480,7 @@ impl Strategy for FuturesClosedPhaseOrderStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Futures { intent: FuturesOrderIntent::open( "IF2501", @@ -506,6 +512,7 @@ impl Strategy for FuturesAboveUpperLimitStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Futures { intent: FuturesOrderIntent::limit_open( "IF2501", @@ -537,6 +544,7 @@ impl Strategy for FuturesDepthLimitOrderStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Futures { intent: FuturesOrderIntent::limit_open( "IF2501", @@ -720,6 +728,7 @@ impl Strategy for LimitCarryStrategy { } self.issued = true; Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -803,6 +812,7 @@ impl Strategy for UniverseDirectiveStrategy { _ => Vec::new(), }; Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -844,6 +854,7 @@ impl Strategy for MinuteProbeStrategy { _ctx: &StrategyContext<'_>, ) -> Result { Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -885,6 +896,7 @@ impl Strategy for MinuteProbeStrategy { } self.ordered = true; Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -987,6 +999,7 @@ impl Strategy for OrderInspectionStrategy { _ctx: &StrategyContext<'_>, ) -> Result { Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1030,6 +1043,7 @@ impl Strategy for AccountFlowStrategy { return Ok(StrategyDecision::default()); } Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4134,6 +4148,7 @@ impl Strategy for BuyMissingRowThenHoldStrategy { ) -> Result { if ctx.execution_date == d(2025, 5, 26) { return Ok(StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index e97244e..26d3b8a 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -209,6 +209,7 @@ fn execute_single_value_order( &mut portfolio, data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -399,6 +400,7 @@ fn broker_executes_explicit_order_value_buy() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -557,6 +559,7 @@ fn broker_delayed_limit_open_sell_uses_minute_price() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -687,6 +690,7 @@ fn broker_executes_order_shares_and_order_lots() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -806,6 +810,7 @@ fn broker_executes_target_shares_like_order_to() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -981,6 +986,7 @@ fn broker_executes_target_portfolio_smart_with_custom_prices() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1139,6 +1145,7 @@ fn broker_executes_target_portfolio_smart_with_algo_order_style() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1254,6 +1261,7 @@ fn broker_executes_order_percent_and_target_percent() { &mut percent_portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1278,6 +1286,7 @@ fn broker_executes_order_percent_and_target_percent() { &mut target_percent_portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1380,6 +1389,7 @@ fn broker_uses_day_open_price_for_open_auction_matching() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1487,6 +1497,7 @@ fn broker_open_auction_uses_auction_volume_without_quote_liquidity() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1590,6 +1601,7 @@ fn broker_cancels_buy_when_open_hits_upper_limit() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1707,6 +1719,7 @@ fn broker_applies_price_ratio_slippage_on_snapshot_fills() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1812,6 +1825,7 @@ fn broker_applies_dynamic_slippage_on_snapshot_fills() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -1933,6 +1947,7 @@ fn broker_applies_tick_size_slippage_on_intraday_last_fills() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2039,6 +2054,7 @@ fn broker_rejects_intraday_last_order_without_execution_quotes() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2163,6 +2179,7 @@ fn broker_executes_intraday_last_on_start_quote_with_trade_delta() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2282,6 +2299,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2398,6 +2416,7 @@ fn broker_cancels_market_buy_when_minute_has_no_volume() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2534,6 +2553,7 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2707,6 +2727,7 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -2888,6 +2909,7 @@ fn broker_executes_algo_vwap_value_with_time_window() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -3036,6 +3058,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -3172,6 +3195,7 @@ fn broker_uses_best_own_price_for_intraday_matching() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -3290,6 +3314,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -3461,6 +3486,7 @@ fn rebalance_optimizer_skips_unfunded_buy_when_existing_position_cannot_sell() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: true, target_weights: BTreeMap::from([("000002.SZ".to_string(), 1.0)]), exit_symbols: BTreeSet::new(), @@ -3657,6 +3683,7 @@ fn rebalance_uses_day_open_for_open_auction_valuation() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: true, target_weights: BTreeMap::from([ ("000001.SZ".to_string(), 0.5), @@ -3841,6 +3868,7 @@ fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: true, target_weights: BTreeMap::from([ ("000001.SZ".to_string(), 0.2), @@ -4025,6 +4053,7 @@ fn rebalance_optimizer_does_not_scale_targets_above_requested_weight() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: true, target_weights: BTreeMap::from([ ("000001.SZ".to_string(), 0.48), @@ -4139,6 +4168,7 @@ fn broker_uses_board_specific_min_quantity_and_step_size_for_buy_sizing() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4244,6 +4274,7 @@ fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4350,6 +4381,7 @@ fn broker_allows_full_odd_lot_sell_when_liquidating_position() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4483,6 +4515,7 @@ fn same_day_sell_then_rebuy_is_rejected_by_default() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4627,6 +4660,7 @@ fn same_day_sell_then_rebuy_can_be_allowed_by_policy() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4680,6 +4714,7 @@ fn broker_configured_policy_can_allow_upper_limit_buy() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4726,6 +4761,7 @@ fn broker_configured_policy_can_allow_lower_limit_sell() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4901,6 +4937,7 @@ fn broker_expires_day_limit_buy_at_market_close() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4941,6 +4978,7 @@ fn broker_expires_day_limit_buy_at_market_close() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -4976,6 +5014,7 @@ fn broker_ioc_limit_order_fills_available_quantity_and_cancels_remainder() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5014,6 +5053,7 @@ fn broker_persists_daily_volume_consumption_across_execute_calls() { .with_liquidity_limit(false); let mut portfolio = PortfolioState::new(1_000_000.0); let decision = || StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: "000002.SZ".to_string(), quantity: 100, @@ -5061,6 +5101,7 @@ fn broker_persists_quote_depth_until_fresh_level_data_arrives() { .with_liquidity_limit(true); let mut portfolio = PortfolioState::new(1_000_000.0); let decision = |quantity| StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: symbol.to_string(), quantity, @@ -5078,6 +5119,7 @@ fn broker_persists_quote_depth_until_fresh_level_data_arrives() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::Shares { symbol: symbol.to_string(), @@ -5182,6 +5224,7 @@ fn broker_persists_quote_volume_participation_until_next_quote() { .with_liquidity_limit(false); let mut portfolio = PortfolioState::new(1_000_000.0); let decision = |quantity| StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::Shares { symbol: symbol.to_string(), quantity, @@ -5264,6 +5307,7 @@ fn broker_day_market_order_cancels_remainder_without_creating_invalid_open_order &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::Shares { symbol: "000002.SZ".to_string(), @@ -5305,6 +5349,7 @@ fn broker_fok_order_is_atomic_when_liquidity_is_insufficient() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::Shares { symbol: "000002.SZ".to_string(), @@ -5351,6 +5396,7 @@ fn broker_fok_order_fills_when_full_quantity_is_available() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::Shares { symbol: "000002.SZ".to_string(), @@ -5388,6 +5434,7 @@ fn broker_gtc_limit_order_survives_close_and_fills_next_day() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5439,6 +5486,7 @@ fn broker_gtc_partial_fills_preserve_cumulative_order_and_commission_state() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5503,6 +5551,7 @@ fn broker_modifies_gtc_limit_order_without_changing_order_identity() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5524,6 +5573,7 @@ fn broker_modifies_gtc_limit_order_without_changing_order_identity() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::ModifyOrder { order_id, new_total_quantity: Some(400), @@ -5591,6 +5641,7 @@ fn broker_modifies_partially_filled_gtc_total_and_preserves_commission_state() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5614,6 +5665,7 @@ fn broker_modifies_partially_filled_gtc_total_and_preserves_commission_state() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::ModifyOrder { order_id, new_total_quantity: Some(200), @@ -5668,6 +5720,7 @@ fn broker_rejected_modify_has_zero_side_effects() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5691,6 +5744,7 @@ fn broker_rejected_modify_has_zero_side_effects() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::ModifyOrder { order_id, new_total_quantity: Some(100), @@ -5728,6 +5782,7 @@ fn broker_accepted_modify_resets_queue_priority_but_reduction_preserves_it() { ); let mut portfolio = PortfolioState::new(1_000_000.0); let create = |reason: &str| StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::LimitShares { symbol: "000002.SZ".to_string(), @@ -5758,6 +5813,7 @@ fn broker_accepted_modify_resets_queue_priority_but_reduction_preserves_it() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::ModifyOrder { order_id: initial_ids[0], new_total_quantity: Some(200), @@ -5787,6 +5843,7 @@ fn broker_accepted_modify_resets_queue_priority_but_reduction_preserves_it() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![OrderIntent::ModifyOrder { order_id: initial_ids[0], new_total_quantity: None, @@ -5828,6 +5885,7 @@ fn broker_rejects_gtc_for_market_order() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), order_intents: vec![ OrderIntent::Shares { symbol: "000002.SZ".to_string(), @@ -5867,6 +5925,7 @@ fn broker_uses_limit_price_slippage_for_limit_orders() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -5905,6 +5964,7 @@ fn broker_rejects_limit_buy_when_final_execution_price_reaches_upper_limit() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -5949,6 +6009,7 @@ fn broker_executes_limit_value_and_limit_percent_intents() { &mut value_portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -5974,6 +6035,7 @@ fn broker_executes_limit_value_and_limit_percent_intents() { &mut percent_portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -6010,6 +6072,7 @@ fn broker_cancels_open_order_by_order_id() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -6033,6 +6096,7 @@ fn broker_cancels_open_order_by_order_id() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -6080,6 +6144,7 @@ fn broker_emits_cancellation_reject_for_unknown_order() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(), @@ -6188,6 +6253,7 @@ fn broker_reserves_sellable_quantity_for_open_limit_sells() { &mut portfolio, &data, &StrategyDecision { + buy_denials: Default::default(), rebalance: false, target_weights: BTreeMap::new(), exit_symbols: BTreeSet::new(),