fix: share explicit execution price selection and reject stale-price fallback
This commit is contained in:
@@ -256,6 +256,21 @@ pub enum MatchingType {
|
|||||||
Twap,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum EquityExecutionPhase {
|
enum EquityExecutionPhase {
|
||||||
ContinuousAuction,
|
ContinuousAuction,
|
||||||
@@ -1353,58 +1368,7 @@ where
|
|||||||
if self.is_post_close_fixed_price(snapshot.date) {
|
if self.is_post_close_fixed_price(snapshot.date) {
|
||||||
return (snapshot.close.is_finite() && snapshot.close > 0.0).then_some(snapshot.close);
|
return (snapshot.close.is_finite() && snapshot.close > 0.0).then_some(snapshot.close);
|
||||||
}
|
}
|
||||||
let raw_price = match matching_type {
|
intraday_reference_price(quote,matching_type,side)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quote_mark_price(&self, quote: &IntradayExecutionQuote, fallback: f64) -> f64 {
|
fn quote_mark_price(&self, quote: &IntradayExecutionQuote, fallback: f64) -> f64 {
|
||||||
@@ -2242,13 +2206,9 @@ where
|
|||||||
};
|
};
|
||||||
let latest = quotes
|
let latest = quotes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|quote| {
|
.filter(|quote| quote.timestamp <= cursor)
|
||||||
quote.timestamp <= cursor
|
|
||||||
&& self
|
|
||||||
.select_quote_reference_price(snapshot, quote, side, matching_type)
|
|
||||||
.is_some()
|
|
||||||
})
|
|
||||||
.max_by_key(|quote| quote.timestamp)?;
|
.max_by_key(|quote| quote.timestamp)?;
|
||||||
|
self.select_quote_reference_price(snapshot,latest,side,matching_type)?;
|
||||||
if require_executable_liquidity
|
if require_executable_liquidity
|
||||||
&& !self.quote_has_executable_liquidity(latest, side, matching_type)
|
&& !self.quote_has_executable_liquidity(latest, side, matching_type)
|
||||||
{
|
{
|
||||||
@@ -7320,27 +7280,6 @@ where
|
|||||||
return Ok(max_fill);
|
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 {
|
if self.volume_limit {
|
||||||
let raw_limit = self.volume_rate.map_err(|error| error.to_string())?
|
let raw_limit = self.volume_rate.map_err(|error| error.to_string())?
|
||||||
.remaining(available_market_volume, u64::from(consumed_turnover), requested_qty);
|
.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]
|
#[test]
|
||||||
fn later_execution_clocks_do_not_replenish_the_same_observed_volume() {
|
fn later_execution_clocks_do_not_replenish_the_same_observed_volume() {
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2025,1,2).unwrap();
|
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 sha2::{Digest, Sha256};
|
||||||
use rhai::{AST, Dynamic, Engine, ImmutableString, Map, Scope};
|
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;
|
use crate::cost::ChinaAShareCostModel;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::data::EligibleUniverseSnapshot;
|
use crate::data::EligibleUniverseSnapshot;
|
||||||
@@ -3090,32 +3090,7 @@ impl PlatformExprStrategy {
|
|||||||
quote: &crate::data::IntradayExecutionQuote,
|
quote: &crate::data::IntradayExecutionQuote,
|
||||||
side: OrderSide,
|
side: OrderSide,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
let last =
|
intraday_reference_price(quote,self.config.matching_type,side)
|
||||||
|| (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(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn projected_execution_limit_rejection_reason(
|
fn projected_execution_limit_rejection_reason(
|
||||||
|
|||||||
Reference in New Issue
Block a user