diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index d23844f..a284f5a 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -362,7 +362,31 @@ where symbol: &str, snapshot: &crate::data::DailyMarketSnapshot, ) -> f64 { - let _ = (date, data, symbol); + if self.aiquant_rqalpha_execution_rules && self.execution_price_field == PriceField::Last + { + let start_cursor = self + .runtime_intraday_start_time + .get() + .or(self.intraday_execution_start_time) + .map(|start_time| date.and_time(start_time)); + let matching_type = self.matching_type_for_algo_request(None); + if let Some(quote) = self.latest_known_quote_at_or_before( + data.execution_quotes_on(date, symbol), + start_cursor, + snapshot, + OrderSide::Buy, + matching_type, + false, + ) { + let fallback = self + .select_quote_reference_price(snapshot, quote, OrderSide::Buy, matching_type) + .unwrap_or(snapshot.last_price); + let mark_price = self.quote_mark_price(quote, fallback); + if mark_price.is_finite() && mark_price > 0.0 { + return mark_price; + } + } + } if snapshot.close.is_finite() && snapshot.close > 0.0 { snapshot.close } else { @@ -5254,6 +5278,129 @@ mod tests { ); } + #[test] + fn aiquant_target_value_valuation_uses_scheduled_tick_last_price() { + let date = chrono::NaiveDate::from_ymd_opt(2023, 5, 8).expect("valid date"); + let symbol = "603101.SH"; + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel { + commission_rate: 0.0003, + stamp_tax_rate_before_change: 0.0005, + stamp_tax_rate_after_change: 0.0005, + minimum_commission: 5.0, + }, + ChinaEquityRuleHooks, + PriceField::Last, + ) + .with_intraday_execution_start_time(date.and_hms_opt(10, 40, 0).unwrap().time()) + .with_slippage_model(SlippageModel::PriceRatio(0.002)) + .with_strict_value_budget(true) + .with_aiquant_rqalpha_execution_rules(true) + .with_volume_limit(false) + .with_liquidity_limit(false) + .with_inactive_limit(false); + let snapshot = DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: Some("2023-05-08 15:00:00".to_string()), + day_open: 5.86, + open: 5.86, + high: 5.90, + low: 5.76, + close: 5.81, + last_price: 5.81, + bid1: 5.82, + ask1: 5.83, + prev_close: 5.85, + volume: 1_000_000, + tick_volume: 262, + bid1_volume: 54, + ask1_volume: 143, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 6.44, + lower_limit: 5.27, + price_tick: 0.01, + }; + let quote = IntradayExecutionQuote { + date, + symbol: symbol.to_string(), + timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"), + last_price: 5.83, + bid1: 5.82, + ask1: 5.83, + bid1_volume: 54, + ask1_volume: 143, + volume_delta: 262, + amount_delta: 152_501.0, + trading_phase: Some("continuous".to_string()), + }; + let data = DataSet::from_components_with_actions_and_quotes( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SH".to_string(), + round_lot: 100, + listed_at: Some(date), + delisted_at: None, + status: "active".to_string(), + }], + vec![snapshot], + Vec::new(), + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1002.0, + prev_close: 998.0, + volume: 1_000_000, + }], + Vec::new(), + vec![quote], + ) + .expect("valid dataset"); + let snapshot = data.market(date, symbol).expect("market snapshot"); + assert_eq!( + broker.target_value_valuation_price(date, &data, symbol, snapshot), + 5.83 + ); + + let mut portfolio = PortfolioState::new(8_261_416.62); + portfolio.position_mut(symbol).buy(date, 21_200, 5.8817); + let mut report = BrokerExecutionReport::default(); + broker + .process_target_value( + date, + &mut portfolio, + &data, + symbol, + 9_996_284.62 * 0.5 / 40.0, + "daily_position_target_adjust", + &mut BTreeMap::new(), + &mut BTreeMap::new(), + &mut None, + &mut BTreeMap::new(), + &mut report, + ) + .expect("process target value"); + + assert_eq!(portfolio.position(symbol).map(|pos| pos.quantity), Some(21_400)); + let order = report.order_events.last().expect("target value order"); + assert_eq!(order.requested_quantity, 200); + assert_eq!(order.filled_quantity, 200); + } + #[test] fn next_tick_last_execution_uses_latest_quote_before_decision_time() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");