fix: share explicit execution price selection and reject stale-price fallback
This commit is contained in:
@@ -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("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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user