- Fixed all FromPrimitive imports across codebase - Resolved all common::types import paths (219+ files) - Fixed Volume constructor issues (type alias vs struct) - Resolved all E0308 type mismatches - Fixed ExecutionReport and BrokerError imports - Added missing Price arithmetic assignment traits - Fixed Decimal to_f64 method calls with ToPrimitive - Eliminated all re-exports per architectural rules Errors reduced from 436 to 106 - 76% reduction achieved
898 lines
34 KiB
Rust
898 lines
34 KiB
Rust
//! Order execution unit tests
|
|
//!
|
|
//! Tests order routing, execution algorithms, and venue selection
|
|
//! with focus on best execution and latency optimization.
|
|
|
|
use common::*;
|
|
// CANONICAL TYPE IMPORTS - Use types::prelude::Decimal
|
|
use std::collections::HashMap;
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_smart_order_router_venue_selection() {
|
|
let mut router = SmartOrderRouter::new();
|
|
|
|
// Add venues with different characteristics
|
|
router.add_venue(Venue {
|
|
id: "NYSE".to_string(),
|
|
latency: Duration::from_millis(2),
|
|
fee_rate: Decimal::from_str("0.0005").unwrap(), // 0.05%
|
|
liquidity_score: 95,
|
|
reliability_score: 99,
|
|
});
|
|
|
|
router.add_venue(Venue {
|
|
id: "NASDAQ".to_string(),
|
|
latency: Duration::from_millis(1),
|
|
fee_rate: Decimal::from_str("0.0007").unwrap(), // 0.07%
|
|
liquidity_score: 90,
|
|
reliability_score: 98,
|
|
});
|
|
|
|
router.add_venue(Venue {
|
|
id: "DARK_POOL".to_string(),
|
|
latency: Duration::from_millis(5),
|
|
fee_rate: Decimal::from_str("0.0002").unwrap(), // 0.02%
|
|
liquidity_score: 70,
|
|
reliability_score: 95,
|
|
});
|
|
|
|
// Small order - should prefer low latency
|
|
let small_order = Order {
|
|
id: OrderId::from("SMALL-001"),
|
|
symbol: Symbol::new("AAPL".to_string()).unwrap(),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: None,
|
|
time_in_force: TimeInForce::IOC,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("CLIENT-001".to_string()),
|
|
};
|
|
|
|
let small_routing = router.select_venue(&small_order, VenueSelectionStrategy::LatencyOptimized).await
|
|
.expect("Should select venue for small order");
|
|
assert_eq!(small_routing.venue_id, "NASDAQ"); // Lowest latency
|
|
|
|
// Large order - should prefer liquidity
|
|
let large_order = Order {
|
|
id: OrderId::from("LARGE-001"),
|
|
symbol: Symbol::new("AAPL".to_string()).unwrap(),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::new(Decimal::from(10000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: Some(Price::new(Decimal::from(150)).unwrap()),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("CLIENT-001".to_string()),
|
|
};
|
|
|
|
let large_routing = router.select_venue(&large_order, VenueSelectionStrategy::LiquidityOptimized).await
|
|
.expect("Should select venue for large order");
|
|
assert_eq!(large_routing.venue_id, "NYSE"); // Highest liquidity
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_twap_execution_algorithm() {
|
|
let mut twap = TwapExecutor::new(
|
|
Duration::from_secs(300), // 5 minutes
|
|
20 // 20 child orders
|
|
);
|
|
|
|
let parent_order = Order {
|
|
id: OrderId::from("TWAP-PARENT"),
|
|
symbol: Symbol::new("MSFT".to_string()).unwrap(),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::new(Decimal::from(10000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: Some(Price::new(Decimal::from(300)).unwrap()),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("INST-001".to_string()),
|
|
};
|
|
|
|
let execution_plan = twap.create_execution_plan(&parent_order).await
|
|
.expect("Should create TWAP execution plan");
|
|
|
|
// Should create 20 child orders
|
|
assert_eq!(execution_plan.child_orders.len(), 20);
|
|
|
|
// Each child order should be 500 shares (10000 / 20)
|
|
for child_order in &execution_plan.child_orders {
|
|
assert_eq!(child_order.quantity.value(), Decimal::from(500));
|
|
}
|
|
|
|
// Should have proper timing intervals (15 seconds each)
|
|
let expected_interval = Duration::from_secs(15); // 300 seconds / 20 orders
|
|
for i in 1..execution_plan.execution_schedule.len() {
|
|
let interval = execution_plan.execution_schedule[i] - execution_plan.execution_schedule[i-1];
|
|
let tolerance = Duration::from_millis(100);
|
|
assert!((interval - expected_interval) < tolerance);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vwap_execution_algorithm() {
|
|
let mut vwap = VwapExecutor::new();
|
|
|
|
// Mock historical volume profile
|
|
let volume_profile = vec![
|
|
(chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(), Decimal::from(1000)), // Market open
|
|
(chrono::NaiveTime::from_hms_opt(10, 0, 0).unwrap(), Decimal::from(800)),
|
|
(chrono::NaiveTime::from_hms_opt(11, 0, 0).unwrap(), Decimal::from(600)),
|
|
(chrono::NaiveTime::from_hms_opt(14, 0, 0).unwrap(), Decimal::from(700)), // Lunch time
|
|
(chrono::NaiveTime::from_hms_opt(15, 30, 0).unwrap(), Decimal::from(1200)), // Market close
|
|
];
|
|
|
|
vwap.set_volume_profile(Symbol::new("GOOGL".to_string()).unwrap(), volume_profile);
|
|
|
|
let parent_order = Order {
|
|
id: OrderId::from("VWAP-PARENT"),
|
|
symbol: Symbol::new("GOOGL".to_string()).unwrap(),
|
|
side: Side::Sell,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::new(Decimal::from(5000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: Some(Price::new(Decimal::from(2500)).unwrap()),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("FUND-001".to_string()),
|
|
};
|
|
|
|
let execution_plan = vwap.create_execution_plan(&parent_order).await
|
|
.expect("Should create VWAP execution plan");
|
|
|
|
// Should weight execution based on historical volume
|
|
// Market open and close should have larger orders
|
|
let open_order = execution_plan.child_orders.iter()
|
|
.find(|o| o.id.value().contains("09:30"))
|
|
.expect("Should have market open order");
|
|
|
|
let close_order = execution_plan.child_orders.iter()
|
|
.find(|o| o.id.value().contains("15:30"))
|
|
.expect("Should have market close order");
|
|
|
|
let midday_order = execution_plan.child_orders.iter()
|
|
.find(|o| o.id.value().contains("11:00"))
|
|
.expect("Should have midday order");
|
|
|
|
// Open and close orders should be larger than midday
|
|
assert!(open_order.quantity.value() > midday_order.quantity.value());
|
|
assert!(close_order.quantity.value() > midday_order.quantity.value());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_implementation_shortfall_execution() {
|
|
let mut is_executor = ImplementationShortfallExecutor::new(
|
|
Decimal::from_str("0.02").unwrap(), // 2% risk aversion
|
|
Duration::from_secs(1800) // 30 minutes
|
|
);
|
|
|
|
let parent_order = Order {
|
|
id: OrderId::from("IS-PARENT"),
|
|
symbol: Symbol::new("TSLA".to_string()).unwrap(),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: Quantity::new(Decimal::from(2000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: None,
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("HEDGE-001".to_string()),
|
|
};
|
|
|
|
// Set market conditions
|
|
let market_conditions = MarketConditions {
|
|
volatility: Decimal::from_str("0.25").unwrap(), // 25% annualized
|
|
spread: Decimal::from_str("0.10").unwrap(), // $0.10 spread
|
|
temporary_impact_rate: Decimal::from_str("0.001").unwrap(),
|
|
permanent_impact_rate: Decimal::from_str("0.0005").unwrap(),
|
|
};
|
|
|
|
is_executor.set_market_conditions(market_conditions);
|
|
|
|
let execution_plan = is_executor.create_execution_plan(&parent_order).await
|
|
.expect("Should create IS execution plan");
|
|
|
|
// Should optimize trade-off between market impact and timing risk
|
|
assert!(!execution_plan.child_orders.is_empty());
|
|
|
|
// In high volatility, should execute faster to reduce timing risk
|
|
let total_execution_time = execution_plan.execution_schedule.last().unwrap() -
|
|
execution_plan.execution_schedule.first().unwrap();
|
|
|
|
assert!(total_execution_time < Duration::from_secs(1800)); // Less than max time
|
|
assert!(total_execution_time > Duration::from_secs(60)); // But not too fast
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_iceberg_order_execution() {
|
|
let mut iceberg = IcebergExecutor::new(
|
|
Quantity::new(Decimal::from(500)).map_err(|e| format!("Failed to create iceberg quantity: {}", e)).unwrap(), // Show 500 shares at a time
|
|
Decimal::from_str("0.01").unwrap() // 1% price improvement threshold
|
|
);
|
|
|
|
let parent_order = Order {
|
|
id: OrderId::from("ICEBERG-PARENT"),
|
|
symbol: Symbol::new("AMZN".to_string()).unwrap(),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::new(Decimal::from(5000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: Some(Price::new(Decimal::from(3000)).unwrap()),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("CLIENT-001".to_string()),
|
|
};
|
|
|
|
let initial_slice = iceberg.get_initial_slice(&parent_order).await
|
|
.expect("Should get initial iceberg slice");
|
|
|
|
// Initial slice should be clip size
|
|
assert_eq!(initial_slice.quantity.value(), Decimal::from(500));
|
|
assert_eq!(initial_slice.price, parent_order.price);
|
|
|
|
// Simulate partial fill
|
|
let fill = Fill {
|
|
id: FillId::new("FILL-ICE-001".to_string()),
|
|
order_id: initial_slice.id.clone(),
|
|
trade_id: TradeId::new("TRADE-ICE-001".to_string()),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
quantity: Quantity::new(Decimal::from(300)).map_err(|e| format!("Failed to create fill quantity: {}", e)).unwrap(),
|
|
price: Price::new(Decimal::from(3000)).unwrap(),
|
|
timestamp: chrono::Utc::now(),
|
|
commission: None,
|
|
};
|
|
|
|
let next_slice = iceberg.handle_fill(&parent_order, &fill).await
|
|
.expect("Should handle iceberg fill");
|
|
|
|
assert!(next_slice.is_some());
|
|
let next = next_slice.unwrap();
|
|
|
|
// Should replenish to show full clip size again
|
|
assert_eq!(next.quantity.value(), Decimal::from(500)); // 200 remaining + 300 new
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execution_quality_measurement() {
|
|
let mut quality_tracker = ExecutionQualityTracker::new();
|
|
|
|
// Record executions with different quality metrics
|
|
let execution1 = ExecutionResult {
|
|
order_id: OrderId::from("EXEC-001"),
|
|
symbol: Symbol::new("AAPL".to_string()).unwrap(),
|
|
expected_price: Price::new(Decimal::from(150)).unwrap(),
|
|
executed_price: Price::new(Decimal::from_str("150.05").unwrap()).unwrap(),
|
|
quantity: Quantity::new(Decimal::from(1000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
execution_time: Duration::from_millis(50),
|
|
venue: "NYSE".to_string(),
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
let execution2 = ExecutionResult {
|
|
order_id: OrderId::from("EXEC-002"),
|
|
symbol: Symbol::new("AAPL".to_string()).unwrap(),
|
|
expected_price: Price::new(Decimal::from(150)).unwrap(),
|
|
executed_price: Price::new(Decimal::from_str("149.98").unwrap()).unwrap(),
|
|
quantity: Quantity::new(Decimal::from(500)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
execution_time: Duration::from_millis(25),
|
|
venue: "NASDAQ".to_string(),
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
quality_tracker.record_execution(execution1);
|
|
quality_tracker.record_execution(execution2);
|
|
|
|
let quality_metrics = quality_tracker.calculate_metrics(Duration::from_secs(3600)).await
|
|
.expect("Should calculate quality metrics");
|
|
|
|
// Should calculate average slippage
|
|
let expected_avg_slippage = (Decimal::from_str("0.05").unwrap() + Decimal::from_str("-0.02").unwrap()) / Decimal::from(2);
|
|
assert_eq!(quality_metrics.average_slippage, expected_avg_slippage);
|
|
|
|
// Should track fill rates and timing
|
|
assert_eq!(quality_metrics.total_executions, 2);
|
|
assert!(quality_metrics.average_execution_time < Duration::from_millis(50));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_multi_venue_execution() {
|
|
let mut multi_venue = MultiVenueExecutor::new();
|
|
|
|
multi_venue.add_venue("NYSE", 0.3); // 30% allocation
|
|
multi_venue.add_venue("NASDAQ", 0.4); // 40% allocation
|
|
multi_venue.add_venue("BATS", 0.2); // 20% allocation
|
|
multi_venue.add_venue("IEX", 0.1); // 10% allocation
|
|
|
|
let large_order = Order {
|
|
id: OrderId::from("MULTI-VENUE"),
|
|
symbol: Symbol::new("SPY".to_string()).unwrap(),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::new(Decimal::from(10000)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(),
|
|
price: Some(Price::new(Decimal::from(400)).unwrap()),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: ClientId::new("INSTITUTION".to_string()),
|
|
};
|
|
|
|
let venue_orders = multi_venue.split_order(&large_order).await
|
|
.expect("Should split order across venues");
|
|
|
|
assert_eq!(venue_orders.len(), 4);
|
|
|
|
// Check allocations
|
|
let nyse_order = venue_orders.iter().find(|o| o.venue_id == "NYSE").unwrap();
|
|
let nasdaq_order = venue_orders.iter().find(|o| o.venue_id == "NASDAQ").unwrap();
|
|
let bats_order = venue_orders.iter().find(|o| o.venue_id == "BATS").unwrap();
|
|
let iex_order = venue_orders.iter().find(|o| o.venue_id == "IEX").unwrap();
|
|
|
|
assert_eq!(nyse_order.quantity.value(), Decimal::from(3000)); // 30%
|
|
assert_eq!(nasdaq_order.quantity.value(), Decimal::from(4000)); // 40%
|
|
assert_eq!(bats_order.quantity.value(), Decimal::from(2000)); // 20%
|
|
assert_eq!(iex_order.quantity.value(), Decimal::from(1000)); // 10%
|
|
|
|
// Total should match original order
|
|
let total_quantity: Decimal = venue_orders.iter()
|
|
.map(|o| o.quantity.value())
|
|
.sum();
|
|
assert_eq!(total_quantity, large_order.quantity.value());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_latency_measurement() {
|
|
let mut latency_tracker = LatencyTracker::new();
|
|
|
|
let start_time = Instant::now();
|
|
|
|
// Simulate order lifecycle with timing points
|
|
let order_id = OrderId::from("LATENCY-TEST");
|
|
|
|
latency_tracker.mark_order_sent(&order_id, start_time);
|
|
|
|
let ack_time = start_time + Duration::from_micros(500); // 500μs to ack
|
|
latency_tracker.mark_order_ack(&order_id, ack_time);
|
|
|
|
let fill_time = start_time + Duration::from_millis(2); // 2ms to fill
|
|
latency_tracker.mark_order_fill(&order_id, fill_time);
|
|
|
|
let metrics = latency_tracker.get_metrics(&order_id).await
|
|
.expect("Should get latency metrics");
|
|
|
|
assert_eq!(metrics.order_to_ack, Duration::from_micros(500));
|
|
assert_eq!(metrics.order_to_fill, Duration::from_millis(2));
|
|
assert_eq!(metrics.ack_to_fill, Duration::from_millis(2) - Duration::from_micros(500));
|
|
}
|
|
}
|
|
|
|
// Production implementations for testing
|
|
#[derive(Debug, Clone)]
|
|
struct Venue {
|
|
id: String,
|
|
latency: Duration,
|
|
fee_rate: Decimal,
|
|
liquidity_score: u32,
|
|
reliability_score: u32,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SmartOrderRouter {
|
|
venues: Vec<Venue>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum VenueSelectionStrategy {
|
|
LatencyOptimized,
|
|
LiquidityOptimized,
|
|
CostOptimized,
|
|
Balanced,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct VenueRoutingDecision {
|
|
venue_id: String,
|
|
expected_latency: Duration,
|
|
expected_fee: Money,
|
|
confidence_score: u32,
|
|
}
|
|
|
|
impl SmartOrderRouter {
|
|
fn new() -> Self {
|
|
Self {
|
|
venues: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn add_venue(&mut self, venue: Venue) {
|
|
self.venues.push(venue);
|
|
}
|
|
|
|
async fn select_venue(&self, order: &Order, strategy: VenueSelectionStrategy) -> Result<VenueRoutingDecision, Box<dyn std::error::Error>> {
|
|
if self.venues.is_empty() {
|
|
return Err("No venues available".into());
|
|
}
|
|
|
|
let best_venue = match strategy {
|
|
VenueSelectionStrategy::LatencyOptimized => {
|
|
self.venues.iter().min_by_key(|v| v.latency).unwrap()
|
|
},
|
|
VenueSelectionStrategy::LiquidityOptimized => {
|
|
self.venues.iter().max_by_key(|v| v.liquidity_score).unwrap()
|
|
},
|
|
VenueSelectionStrategy::CostOptimized => {
|
|
self.venues.iter().min_by_key(|v| v.fee_rate).unwrap()
|
|
},
|
|
VenueSelectionStrategy::Balanced => {
|
|
// Simple scoring: combine latency, liquidity, and cost
|
|
self.venues.iter().max_by_key(|v| {
|
|
let latency_score = 100 - (v.latency.as_millis() as u32);
|
|
let cost_score = 100 - (v.fee_rate * Decimal::from(10000)).to_u32().unwrap_or(0);
|
|
latency_score + v.liquidity_score + cost_score + v.reliability_score
|
|
}).unwrap()
|
|
}
|
|
};
|
|
|
|
let order_value = order.quantity.value() *
|
|
order.price.as_ref().unwrap_or(&Price::new(Decimal::from(100)).unwrap()).value();
|
|
|
|
Ok(VenueRoutingDecision {
|
|
venue_id: best_venue.id.clone(),
|
|
expected_latency: best_venue.latency,
|
|
expected_fee: Money::new(order_value * best_venue.fee_rate, Currency::USD),
|
|
confidence_score: best_venue.reliability_score,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TwapExecutor {
|
|
duration: Duration,
|
|
num_slices: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ExecutionPlan {
|
|
child_orders: Vec<Order>,
|
|
execution_schedule: Vec<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
impl TwapExecutor {
|
|
fn new(duration: Duration, num_slices: usize) -> Self {
|
|
Self { duration, num_slices }
|
|
}
|
|
|
|
async fn create_execution_plan(&self, parent_order: &Order) -> Result<ExecutionPlan, Box<dyn std::error::Error>> {
|
|
let slice_size = parent_order.quantity.value() / Decimal::from(self.num_slices);
|
|
let slice_interval = self.duration / self.num_slices as u32;
|
|
|
|
let mut child_orders = Vec::new();
|
|
let mut execution_schedule = Vec::new();
|
|
|
|
let start_time = chrono::Utc::now();
|
|
|
|
for i in 0..self.num_slices {
|
|
let child_order = Order {
|
|
id: OrderId::new(format!("{}-TWAP-{}", parent_order.id.value(), i)),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
order_type: parent_order.order_type,
|
|
quantity: Quantity::new(slice_size).map_err(|e| format!("Failed to create slice quantity: {}", e)).unwrap(),
|
|
price: parent_order.price.clone(),
|
|
time_in_force: TimeInForce::IOC,
|
|
timestamp: parent_order.timestamp,
|
|
status: OrderStatus::New,
|
|
client_id: parent_order.client_id.clone(),
|
|
};
|
|
|
|
child_orders.push(child_order);
|
|
execution_schedule.push(start_time + chrono::Duration::from_std(slice_interval * i as u32).unwrap());
|
|
}
|
|
|
|
Ok(ExecutionPlan {
|
|
child_orders,
|
|
execution_schedule,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct VwapExecutor {
|
|
volume_profiles: HashMap<Symbol, Vec<(chrono::NaiveTime, Decimal)>>,
|
|
}
|
|
|
|
impl VwapExecutor {
|
|
fn new() -> Self {
|
|
Self {
|
|
volume_profiles: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn set_volume_profile(&mut self, symbol: Symbol, profile: Vec<(chrono::NaiveTime, Decimal)>) {
|
|
self.volume_profiles.insert(symbol, profile);
|
|
}
|
|
|
|
async fn create_execution_plan(&self, parent_order: &Order) -> Result<ExecutionPlan, Box<dyn std::error::Error>> {
|
|
let profile = self.volume_profiles.get(&parent_order.symbol)
|
|
.ok_or("No volume profile for symbol")?;
|
|
|
|
let total_volume: Decimal = profile.iter().map(|(_, vol)| *vol).sum();
|
|
|
|
let mut child_orders = Vec::new();
|
|
let mut execution_schedule = Vec::new();
|
|
|
|
let base_date = chrono::Utc::now().date_naive();
|
|
|
|
for (time, historical_volume) in profile {
|
|
let weight = historical_volume / total_volume;
|
|
let slice_quantity = parent_order.quantity.value() * weight;
|
|
|
|
if slice_quantity > Decimal::ZERO {
|
|
let child_order = Order {
|
|
id: OrderId::new(format!("{}-VWAP-{}", parent_order.id.value(), time.format("%H:%M"))),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
order_type: parent_order.order_type,
|
|
quantity: Quantity::new(slice_quantity).map_err(|e| format!("Failed to create slice quantity: {}", e)).unwrap(),
|
|
price: parent_order.price.clone(),
|
|
time_in_force: TimeInForce::IOC,
|
|
timestamp: parent_order.timestamp,
|
|
status: OrderStatus::New,
|
|
client_id: parent_order.client_id.clone(),
|
|
};
|
|
|
|
child_orders.push(child_order);
|
|
|
|
let execution_time = chrono::DateTime::from_naive_utc_and_offset(
|
|
base_date.and_time(*time),
|
|
chrono::Utc
|
|
);
|
|
execution_schedule.push(execution_time);
|
|
}
|
|
}
|
|
|
|
Ok(ExecutionPlan {
|
|
child_orders,
|
|
execution_schedule,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ImplementationShortfallExecutor {
|
|
risk_aversion: Decimal,
|
|
max_duration: Duration,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct MarketConditions {
|
|
volatility: Decimal,
|
|
spread: Decimal,
|
|
temporary_impact_rate: Decimal,
|
|
permanent_impact_rate: Decimal,
|
|
}
|
|
|
|
impl ImplementationShortfallExecutor {
|
|
fn new(risk_aversion: Decimal, max_duration: Duration) -> Self {
|
|
Self { risk_aversion, max_duration }
|
|
}
|
|
|
|
fn set_market_conditions(&mut self, _conditions: MarketConditions) {
|
|
// Store market conditions for optimization
|
|
}
|
|
|
|
async fn create_execution_plan(&self, parent_order: &Order) -> Result<ExecutionPlan, Box<dyn std::error::Error>> {
|
|
// Simplified IS optimization - in practice would solve for optimal trajectory
|
|
let num_slices = if parent_order.quantity.value() > Decimal::from(5000) { 10 } else { 5 };
|
|
let execution_duration = self.max_duration / 2; // Execute faster in volatile markets
|
|
|
|
let slice_size = parent_order.quantity.value() / Decimal::from(num_slices);
|
|
let slice_interval = execution_duration / num_slices as u32;
|
|
|
|
let mut child_orders = Vec::new();
|
|
let mut execution_schedule = Vec::new();
|
|
|
|
let start_time = chrono::Utc::now();
|
|
|
|
for i in 0..num_slices {
|
|
let child_order = Order {
|
|
id: OrderId::new(format!("{}-IS-{}", parent_order.id.value(), i)),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
order_type: OrderType::Limit, // Use limit orders for better control
|
|
quantity: Quantity::new(slice_size).map_err(|e| format!("Failed to create slice quantity: {}", e)).unwrap(),
|
|
price: parent_order.price.clone(),
|
|
time_in_force: TimeInForce::IOC,
|
|
timestamp: parent_order.timestamp,
|
|
status: OrderStatus::New,
|
|
client_id: parent_order.client_id.clone(),
|
|
};
|
|
|
|
child_orders.push(child_order);
|
|
execution_schedule.push(start_time + chrono::Duration::from_std(slice_interval * i as u32).unwrap());
|
|
}
|
|
|
|
Ok(ExecutionPlan {
|
|
child_orders,
|
|
execution_schedule,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct IcebergExecutor {
|
|
clip_size: Quantity,
|
|
price_improvement_threshold: Decimal,
|
|
}
|
|
|
|
impl IcebergExecutor {
|
|
fn new(clip_size: Quantity, price_improvement_threshold: Decimal) -> Self {
|
|
Self { clip_size, price_improvement_threshold }
|
|
}
|
|
|
|
async fn get_initial_slice(&self, parent_order: &Order) -> Result<Order, Box<dyn std::error::Error>> {
|
|
let slice_quantity = std::cmp::min(parent_order.quantity.value(), self.clip_size.value());
|
|
|
|
Ok(Order {
|
|
id: OrderId::new(format!("{}-ICE-0", parent_order.id.value())),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
order_type: parent_order.order_type,
|
|
quantity: Quantity::new(slice_quantity).map_err(|e| format!("Failed to create slice quantity: {}", e)).unwrap(),
|
|
price: parent_order.price.clone(),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: parent_order.timestamp,
|
|
status: OrderStatus::New,
|
|
client_id: parent_order.client_id.clone(),
|
|
})
|
|
}
|
|
|
|
async fn handle_fill(&self, parent_order: &Order, _fill: &Fill) -> Result<Option<Order>, Box<dyn std::error::Error>> {
|
|
// Replenish the visible quantity
|
|
Ok(Some(Order {
|
|
id: OrderId::new(format!("{}-ICE-REFILL", parent_order.id.value())),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
order_type: parent_order.order_type,
|
|
quantity: self.clip_size.clone(),
|
|
price: parent_order.price.clone(),
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
status: OrderStatus::New,
|
|
client_id: parent_order.client_id.clone(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ExecutionResult {
|
|
order_id: OrderId,
|
|
symbol: Symbol,
|
|
expected_price: Price,
|
|
executed_price: Price,
|
|
quantity: Quantity,
|
|
execution_time: Duration,
|
|
venue: String,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ExecutionQualityMetrics {
|
|
average_slippage: Decimal,
|
|
total_executions: usize,
|
|
average_execution_time: Duration,
|
|
fill_rate: Decimal,
|
|
venue_performance: HashMap<String, VenueMetrics>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct VenueMetrics {
|
|
average_slippage: Decimal,
|
|
execution_count: usize,
|
|
average_latency: Duration,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ExecutionQualityTracker {
|
|
executions: Vec<ExecutionResult>,
|
|
}
|
|
|
|
impl ExecutionQualityTracker {
|
|
fn new() -> Self {
|
|
Self {
|
|
executions: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn record_execution(&mut self, execution: ExecutionResult) {
|
|
self.executions.push(execution);
|
|
}
|
|
|
|
async fn calculate_metrics(&self, period: Duration) -> Result<ExecutionQualityMetrics, Box<dyn std::error::Error>> {
|
|
let cutoff_time = chrono::Utc::now() - chrono::Duration::from_std(period).unwrap();
|
|
let recent_executions: Vec<_> = self.executions.iter()
|
|
.filter(|e| e.timestamp > cutoff_time)
|
|
.collect();
|
|
|
|
if recent_executions.is_empty() {
|
|
return Err("No executions in specified period".into());
|
|
}
|
|
|
|
// Calculate average slippage
|
|
let total_slippage: Decimal = recent_executions.iter()
|
|
.map(|e| e.executed_price.value() - e.expected_price.value())
|
|
.sum();
|
|
let average_slippage = total_slippage / Decimal::from(recent_executions.len());
|
|
|
|
// Calculate average execution time
|
|
let total_time: Duration = recent_executions.iter()
|
|
.map(|e| e.execution_time)
|
|
.sum();
|
|
let average_execution_time = total_time / recent_executions.len() as u32;
|
|
|
|
// Build venue performance map
|
|
let mut venue_performance = HashMap::new();
|
|
let mut venue_groups: HashMap<String, Vec<&ExecutionResult>> = HashMap::new();
|
|
|
|
for exec in &recent_executions {
|
|
venue_groups.entry(exec.venue.clone()).or_default().push(exec);
|
|
}
|
|
|
|
for (venue, execs) in venue_groups {
|
|
let venue_slippage: Decimal = execs.iter()
|
|
.map(|e| e.executed_price.value() - e.expected_price.value())
|
|
.sum::<Decimal>() / Decimal::from(execs.len());
|
|
|
|
let venue_latency = execs.iter()
|
|
.map(|e| e.execution_time)
|
|
.sum::<Duration>() / execs.len() as u32;
|
|
|
|
venue_performance.insert(venue, VenueMetrics {
|
|
average_slippage: venue_slippage,
|
|
execution_count: execs.len(),
|
|
average_latency: venue_latency,
|
|
});
|
|
}
|
|
|
|
Ok(ExecutionQualityMetrics {
|
|
average_slippage,
|
|
total_executions: recent_executions.len(),
|
|
average_execution_time,
|
|
fill_rate: Decimal::ONE, // Simplified - assume all filled
|
|
venue_performance,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct MultiVenueExecutor {
|
|
venue_allocations: HashMap<String, f64>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct VenueOrder {
|
|
venue_id: String,
|
|
order: Order,
|
|
quantity: Quantity,
|
|
}
|
|
|
|
impl MultiVenueExecutor {
|
|
fn new() -> Self {
|
|
Self {
|
|
venue_allocations: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn add_venue(&mut self, venue_id: &str, allocation: f64) {
|
|
self.venue_allocations.insert(venue_id.to_string(), allocation);
|
|
}
|
|
|
|
async fn split_order(&self, parent_order: &Order) -> Result<Vec<VenueOrder>, Box<dyn std::error::Error>> {
|
|
let mut venue_orders = Vec::new();
|
|
|
|
for (venue_id, &allocation) in &self.venue_allocations {
|
|
let venue_quantity = parent_order.quantity.value() * Decimal::from_f64(allocation).map_err(|e| format!("Failed to convert allocation to Decimal: {}", e)).unwrap();
|
|
|
|
if venue_quantity > Decimal::ZERO {
|
|
let venue_order = Order {
|
|
id: OrderId::new(format!("{}-{}", parent_order.id.value(), venue_id)),
|
|
symbol: parent_order.symbol.clone(),
|
|
side: parent_order.side,
|
|
order_type: parent_order.order_type,
|
|
quantity: Quantity::new(venue_quantity).map_err(|e| format!("Failed to create venue quantity: {}", e)).unwrap(),
|
|
price: parent_order.price.clone(),
|
|
time_in_force: parent_order.time_in_force,
|
|
timestamp: parent_order.timestamp,
|
|
status: OrderStatus::New,
|
|
client_id: parent_order.client_id.clone(),
|
|
};
|
|
|
|
venue_orders.push(VenueOrder {
|
|
venue_id: venue_id.clone(),
|
|
order: venue_order,
|
|
quantity: Quantity::new(venue_quantity).map_err(|e| format!("Failed to create venue quantity: {}", e)).unwrap(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(venue_orders)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LatencyMetrics {
|
|
order_to_ack: Duration,
|
|
order_to_fill: Duration,
|
|
ack_to_fill: Duration,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LatencyTracker {
|
|
timing_points: HashMap<OrderId, TimingPoints>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TimingPoints {
|
|
order_sent: Instant,
|
|
order_ack: Option<Instant>,
|
|
order_fill: Option<Instant>,
|
|
}
|
|
|
|
impl LatencyTracker {
|
|
fn new() -> Self {
|
|
Self {
|
|
timing_points: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn mark_order_sent(&mut self, order_id: &OrderId, time: Instant) {
|
|
self.timing_points.insert(order_id.clone(), TimingPoints {
|
|
order_sent: time,
|
|
order_ack: None,
|
|
order_fill: None,
|
|
});
|
|
}
|
|
|
|
fn mark_order_ack(&mut self, order_id: &OrderId, time: Instant) {
|
|
if let Some(points) = self.timing_points.get_mut(order_id) {
|
|
points.order_ack = Some(time);
|
|
}
|
|
}
|
|
|
|
fn mark_order_fill(&mut self, order_id: &OrderId, time: Instant) {
|
|
if let Some(points) = self.timing_points.get_mut(order_id) {
|
|
points.order_fill = Some(time);
|
|
}
|
|
}
|
|
|
|
async fn get_metrics(&self, order_id: &OrderId) -> Result<LatencyMetrics, Box<dyn std::error::Error>> {
|
|
let points = self.timing_points.get(order_id)
|
|
.ok_or("Order not found in timing points")?;
|
|
|
|
let order_to_ack = points.order_ack
|
|
.ok_or("Order acknowledgment not recorded")?
|
|
.duration_since(points.order_sent);
|
|
|
|
let order_to_fill = points.order_fill
|
|
.ok_or("Order fill not recorded")?
|
|
.duration_since(points.order_sent);
|
|
|
|
let ack_to_fill = points.order_fill.unwrap()
|
|
.duration_since(points.order_ack.unwrap());
|
|
|
|
Ok(LatencyMetrics {
|
|
order_to_ack,
|
|
order_to_fill,
|
|
ack_to_fill,
|
|
})
|
|
}
|
|
} |