diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 8ff50c9..529e42b 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -256,6 +256,21 @@ pub enum MatchingType { Twap, } +pub(crate) fn intraday_reference_price( + quote: &IntradayExecutionQuote, + matching_type: MatchingType, + side: OrderSide, +) -> Option { + let price = match (matching_type, side) { + (MatchingType::MinuteBestOwn, OrderSide::Buy) + | (MatchingType::MinuteBestCounterparty, OrderSide::Sell) => quote.bid1, + (MatchingType::MinuteBestOwn, OrderSide::Sell) + | (MatchingType::MinuteBestCounterparty, OrderSide::Buy) => quote.ask1, + _ => quote.last_price, + }; + (price.is_finite() && price > 0.0).then_some(price) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum EquityExecutionPhase { ContinuousAuction, @@ -1353,58 +1368,7 @@ where if self.is_post_close_fixed_price(snapshot.date) { return (snapshot.close.is_finite() && snapshot.close > 0.0).then_some(snapshot.close); } - let raw_price = match matching_type { - MatchingType::MinuteBestOwn => match side { - OrderSide::Buy => { - if quote.bid1.is_finite() && quote.bid1 > 0.0 { - Some(quote.bid1) - } else { - quote - .last_price - .is_finite() - .then_some(quote.last_price) - .filter(|price| *price > 0.0) - } - } - OrderSide::Sell => { - if quote.ask1.is_finite() && quote.ask1 > 0.0 { - Some(quote.ask1) - } else { - quote - .last_price - .is_finite() - .then_some(quote.last_price) - .filter(|price| *price > 0.0) - } - } - }, - MatchingType::MinuteBestCounterparty => match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - }, - MatchingType::CurrentBarClose - | MatchingType::MinuteLast - | MatchingType::Vwap - | MatchingType::Twap => { - if quote.last_price.is_finite() && quote.last_price > 0.0 { - Some(quote.last_price) - } else { - match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - } - } - } - _ => match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - }, - }?; - if raw_price.is_finite() && raw_price > 0.0 { - Some(raw_price) - } else { - None - } + intraday_reference_price(quote,matching_type,side) } fn quote_mark_price(&self, quote: &IntradayExecutionQuote, fallback: f64) -> f64 { @@ -2242,13 +2206,9 @@ where }; let latest = quotes .iter() - .filter(|quote| { - quote.timestamp <= cursor - && self - .select_quote_reference_price(snapshot, quote, side, matching_type) - .is_some() - }) + .filter(|quote| quote.timestamp <= cursor) .max_by_key(|quote| quote.timestamp)?; + self.select_quote_reference_price(snapshot,latest,side,matching_type)?; if require_executable_liquidity && !self.quote_has_executable_liquidity(latest, side, matching_type) { @@ -7320,27 +7280,6 @@ where return Ok(max_fill); } - if self.liquidity_limit && uses_intraday_quantity && !self.is_open_auction_matching() { - let top_level_liquidity = match side { - OrderSide::Buy => snapshot.liquidity_for_buy(), - OrderSide::Sell => snapshot.liquidity_for_sell(), - } - .min(u32::MAX as u64) as u32; - if top_level_liquidity == 0 { - return Err("no quote liquidity".to_string()); - } - let top_level_limit = if side == OrderSide::Sell && allow_odd_lot_sell { - top_level_liquidity - } else { - self.round_buy_quantity( - top_level_liquidity, - minimum_order_quantity, - order_step_size, - ) - }; - max_fill = max_fill.min(top_level_limit); - } - if self.volume_limit { let raw_limit = self.volume_rate.map_err(|error| error.to_string())? .remaining(available_market_volume, u64::from(consumed_turnover), requested_qty); @@ -11612,6 +11551,24 @@ mod tests { ); } + #[test] + fn explicit_quote_price_models_do_not_replace_missing_prices_or_reuse_older_ones() { + let mut quote = limit_test_quote(10.2,10.1,10.3); + assert_eq!(super::intraday_reference_price("e,MatchingType::CurrentBarClose,OrderSide::Buy),Some(10.2)); + assert_eq!(super::intraday_reference_price("e,MatchingType::MinuteBestOwn,OrderSide::Buy),Some(10.1)); + assert_eq!(super::intraday_reference_price("e,MatchingType::MinuteBestCounterparty,OrderSide::Buy),Some(10.3)); + let old = quote.clone(); + quote.timestamp += chrono::Duration::seconds(1); + quote.ask1 = 0.0; + assert!(super::intraday_reference_price("e,MatchingType::MinuteBestCounterparty,OrderSide::Buy).is_none()); + let broker = BrokerSimulator::new(ChinaAShareCostModel::default(),ChinaEquityRuleHooks); + let snapshot = limit_test_snapshot(); + let at = quote.timestamp; + assert!(broker.latest_known_quote_at_or_before(&[old,quote.clone()],Some(at),&snapshot,OrderSide::Buy,MatchingType::MinuteBestCounterparty,false).is_none()); + quote.last_price = f64::NAN; + assert!(super::intraday_reference_price("e,MatchingType::MinuteLast,OrderSide::Sell).is_none()); + } + #[test] fn later_execution_clocks_do_not_replenish_the_same_observed_volume() { let date = chrono::NaiveDate::from_ymd_opt(2025,1,2).unwrap(); diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index ae748b1..a115dfc 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -7,7 +7,7 @@ use chrono::{Datelike, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTim use sha2::{Digest, Sha256}; use rhai::{AST, Dynamic, Engine, ImmutableString, Map, Scope}; -use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel}; +use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel, intraday_reference_price}; use crate::cost::ChinaAShareCostModel; #[cfg(test)] use crate::data::EligibleUniverseSnapshot; @@ -3090,32 +3090,7 @@ impl PlatformExprStrategy { quote: &crate::data::IntradayExecutionQuote, side: OrderSide, ) -> Option { - let last = - || (quote.last_price.is_finite() && quote.last_price > 0.0).then_some(quote.last_price); - match self.config.matching_type { - MatchingType::MinuteBestOwn => match side { - OrderSide::Buy => (quote.bid1.is_finite() && quote.bid1 > 0.0) - .then_some(quote.bid1) - .or_else(last), - OrderSide::Sell => (quote.ask1.is_finite() && quote.ask1 > 0.0) - .then_some(quote.ask1) - .or_else(last), - }, - MatchingType::MinuteBestCounterparty => match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - }, - MatchingType::MinuteLast | MatchingType::Vwap | MatchingType::Twap => { - last().or_else(|| match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - }) - } - _ => match side { - OrderSide::Buy => quote.buy_price(), - OrderSide::Sell => quote.sell_price(), - }, - } + intraday_reference_price(quote,self.config.matching_type,side) } fn projected_execution_limit_rejection_reason(