Add futures exchange validators
This commit is contained in:
@@ -48,6 +48,25 @@ pub struct BacktestConfig {
|
||||
pub execution_price_field: PriceField,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FuturesValidationConfig {
|
||||
pub enforce_active_instrument: bool,
|
||||
pub enforce_trading_phase: bool,
|
||||
pub enforce_limit_price_tick: bool,
|
||||
pub enforce_price_limits: bool,
|
||||
}
|
||||
|
||||
impl Default for FuturesValidationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enforce_active_instrument: true,
|
||||
enforce_trading_phase: true,
|
||||
enforce_limit_price_tick: true,
|
||||
enforce_price_limits: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DailyEquityPoint {
|
||||
#[serde(with = "date_format")]
|
||||
@@ -294,6 +313,7 @@ pub struct BacktestEngine<S, C, R> {
|
||||
futures_expirations: BTreeMap<NaiveDate, BTreeMap<String, f64>>,
|
||||
futures_settlement_price_mode: String,
|
||||
futures_cost_model: FuturesTransactionCostModel,
|
||||
futures_validation_config: FuturesValidationConfig,
|
||||
}
|
||||
|
||||
impl<S, C, R> BacktestEngine<S, C, R> {
|
||||
@@ -318,6 +338,7 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
||||
futures_expirations: BTreeMap::new(),
|
||||
futures_settlement_price_mode: "close".to_string(),
|
||||
futures_cost_model: FuturesTransactionCostModel::default(),
|
||||
futures_validation_config: FuturesValidationConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +398,11 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_futures_validation_config(mut self, config: FuturesValidationConfig) -> Self {
|
||||
self.futures_validation_config = config;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn process_event_bus_mut(&mut self) -> &mut ProcessEventBus {
|
||||
&mut self.process_event_bus
|
||||
}
|
||||
@@ -832,12 +858,12 @@ where
|
||||
);
|
||||
};
|
||||
|
||||
if let Some(reason) = self.validate_futures_submission(&intent) {
|
||||
let original_requested = intent.quantity;
|
||||
let mut intent = self.resolve_futures_trading_parameters(date, intent);
|
||||
if let Some(reason) = self.validate_futures_submission(date, &intent) {
|
||||
return self.reject_futures_order(date, order_id, intent, reason);
|
||||
}
|
||||
|
||||
let original_requested = intent.quantity;
|
||||
let mut intent = self.resolve_futures_trading_parameters(date, intent);
|
||||
let fill = self.resolve_futures_fill(date, &intent);
|
||||
let Some((execution_price, fill_quantity)) = fill else {
|
||||
if intent.allow_pending || intent.limit_price.is_some() {
|
||||
@@ -1020,14 +1046,76 @@ where
|
||||
report
|
||||
}
|
||||
|
||||
fn validate_futures_submission(&self, intent: &FuturesOrderIntent) -> Option<String> {
|
||||
fn validate_futures_submission(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
intent: &FuturesOrderIntent,
|
||||
) -> Option<String> {
|
||||
if intent.quantity == 0 {
|
||||
return Some("zero futures quantity".to_string());
|
||||
}
|
||||
if self.futures_validation_config.enforce_active_instrument {
|
||||
if let Some(instrument) = self.data.instrument(&intent.symbol) {
|
||||
if !instrument.is_active_on(date) {
|
||||
return Some(format!(
|
||||
"inactive futures instrument symbol={} date={date}",
|
||||
intent.symbol
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.futures_validation_config.enforce_trading_phase {
|
||||
if let Some(snapshot) = self.data.market(date, &intent.symbol) {
|
||||
if snapshot.paused {
|
||||
return Some(format!(
|
||||
"paused futures instrument symbol={}",
|
||||
intent.symbol
|
||||
));
|
||||
}
|
||||
if !futures_trading_phase_allows_orders(snapshot.trading_phase.as_deref()) {
|
||||
return Some(format!(
|
||||
"futures trading phase does not allow orders symbol={} phase={}",
|
||||
intent.symbol,
|
||||
snapshot.trading_phase.as_deref().unwrap_or("")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(limit_price) = intent.limit_price {
|
||||
if !limit_price.is_finite() || limit_price <= 0.0 {
|
||||
return Some("invalid futures limit price".to_string());
|
||||
}
|
||||
if self.futures_validation_config.enforce_limit_price_tick {
|
||||
let tick = self.futures_price_tick(date, &intent.symbol);
|
||||
if !price_is_tick_aligned(limit_price, tick) {
|
||||
return Some(format!(
|
||||
"futures limit price not aligned to tick symbol={} price={limit_price:.6} tick={tick:.6}",
|
||||
intent.symbol
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.futures_validation_config.enforce_price_limits {
|
||||
if let Some(snapshot) = self.data.market(date, &intent.symbol) {
|
||||
if snapshot.upper_limit.is_finite()
|
||||
&& snapshot.upper_limit > 0.0
|
||||
&& limit_price > snapshot.upper_limit + 1e-9
|
||||
{
|
||||
return Some(format!(
|
||||
"futures limit price above upper limit symbol={} price={limit_price:.6} upper={:.6}",
|
||||
intent.symbol, snapshot.upper_limit
|
||||
));
|
||||
}
|
||||
if snapshot.lower_limit.is_finite()
|
||||
&& snapshot.lower_limit > 0.0
|
||||
&& limit_price < snapshot.lower_limit - 1e-9
|
||||
{
|
||||
return Some(format!(
|
||||
"futures limit price below lower limit symbol={} price={limit_price:.6} lower={:.6}",
|
||||
intent.symbol, snapshot.lower_limit
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for order in &self.futures_open_orders {
|
||||
if order.intent.symbol != intent.symbol || order.intent.side() == intent.side() {
|
||||
continue;
|
||||
@@ -1048,6 +1136,20 @@ where
|
||||
None
|
||||
}
|
||||
|
||||
fn futures_price_tick(&self, date: NaiveDate, symbol: &str) -> f64 {
|
||||
self.data
|
||||
.futures_trading_parameter(date, symbol)
|
||||
.map(|params| params.price_tick)
|
||||
.filter(|tick| tick.is_finite() && *tick > 0.0)
|
||||
.or_else(|| {
|
||||
self.data
|
||||
.market(date, symbol)
|
||||
.map(|snapshot| snapshot.effective_price_tick())
|
||||
})
|
||||
.unwrap_or(1.0)
|
||||
.max(1e-9)
|
||||
}
|
||||
|
||||
fn resolve_futures_trading_parameters(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -3226,6 +3328,30 @@ fn analyzer_ratio_change(start: f64, end: f64) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn price_is_tick_aligned(price: f64, tick: f64) -> bool {
|
||||
if !price.is_finite() || !tick.is_finite() || tick <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
let ratio = price / tick;
|
||||
(ratio - ratio.round()).abs() <= 1e-6
|
||||
}
|
||||
|
||||
fn futures_trading_phase_allows_orders(phase: Option<&str>) -> bool {
|
||||
let Some(phase) = phase.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return true;
|
||||
};
|
||||
matches!(
|
||||
phase.to_ascii_lowercase().as_str(),
|
||||
"continuous"
|
||||
| "trading"
|
||||
| "trade"
|
||||
| "open_auction"
|
||||
| "auction"
|
||||
| "call_auction"
|
||||
| "opening_auction"
|
||||
)
|
||||
}
|
||||
|
||||
fn futures_limit_satisfied(side: OrderSide, price: f64, limit_price: Option<f64>) -> bool {
|
||||
let Some(limit_price) = limit_price else {
|
||||
return price.is_finite() && price > 0.0;
|
||||
|
||||
Reference in New Issue
Block a user