fix: share explicit execution price selection and reject stale-price fallback

This commit is contained in:
boris
2026-09-11 15:44:38 +08:00
parent bd389de57f
commit 0fba16342f
2 changed files with 38 additions and 106 deletions
+36 -79
View File
@@ -256,6 +256,21 @@ pub enum MatchingType {
Twap,
}
pub(crate) fn intraday_reference_price(
quote: &IntradayExecutionQuote,
matching_type: MatchingType,
side: OrderSide,
) -> Option<f64> {
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(&quote,MatchingType::CurrentBarClose,OrderSide::Buy),Some(10.2));
assert_eq!(super::intraday_reference_price(&quote,MatchingType::MinuteBestOwn,OrderSide::Buy),Some(10.1));
assert_eq!(super::intraday_reference_price(&quote,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(&quote,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(&quote,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();
+2 -27
View File
@@ -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<f64> {
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(