510 lines
18 KiB
Rust
510 lines
18 KiB
Rust
//! Best Execution Compliance Tests
|
|
//!
|
|
//! Comprehensive tests for MiFID II Best Execution requirements including:
|
|
//! - Execution quality metrics calculation
|
|
//! - Venue selection and analysis
|
|
//! - Transaction cost breakdown
|
|
//! - Best execution policy compliance
|
|
//! - RTS 28 reporting requirements
|
|
|
|
use trading_engine::compliance::best_execution::{
|
|
BestExecutionAnalyzer, BestExecutionConfig, VenueType, ExecutionFactors,
|
|
VenueSelectionCriteria, ReportingIntervals,
|
|
};
|
|
use trading_engine::compliance::{OrderInfo, MiFIDConfig};
|
|
use common::{OrderId, OrderSide, OrderType, Price, Quantity};
|
|
use rust_decimal::Decimal;
|
|
use chrono::Utc;
|
|
|
|
/// Test best execution analyzer initialization
|
|
#[tokio::test]
|
|
async fn test_best_execution_analyzer_initialization() {
|
|
let config = MiFIDConfig {
|
|
best_execution_enabled: true,
|
|
transaction_reporting_endpoint: Some("https://test.endpoint.com".to_string()),
|
|
client_categorization_enabled: true,
|
|
product_governance_enabled: true,
|
|
position_limit_monitoring: true,
|
|
};
|
|
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
// Verify analyzer is properly initialized (no panic)
|
|
assert!(true, "Analyzer initialized successfully");
|
|
}
|
|
|
|
/// Test execution quality metrics calculation
|
|
#[tokio::test]
|
|
async fn test_execution_quality_metrics() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(1000),
|
|
price: Some(Price::from_f64(100.50).expect("Valid price")),
|
|
symbol: "AAPL".to_string(),
|
|
client_id: "CLIENT001".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let result = analyzer.analyze_best_execution(&order_info).await;
|
|
|
|
assert!(result.is_ok(), "Best execution analysis should succeed");
|
|
|
|
let analysis = result.unwrap();
|
|
assert_eq!(analysis.order_id, order_info.order_id);
|
|
assert!(analysis.execution_score >= 0.0 && analysis.execution_score <= 1.0,
|
|
"Execution score should be between 0 and 1");
|
|
|
|
// Verify quality metrics are populated
|
|
let metrics = &analysis.quality_metrics;
|
|
assert!(metrics.fill_rate >= 0.0 && metrics.fill_rate <= 1.0,
|
|
"Fill rate should be a valid percentage");
|
|
assert!(metrics.avg_execution_time_ms > 0,
|
|
"Execution time should be positive");
|
|
}
|
|
|
|
/// Test venue selection and scoring
|
|
#[tokio::test]
|
|
async fn test_venue_selection() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: Quantity::from_shares(5000),
|
|
price: None, // Market order
|
|
symbol: "MSFT".to_string(),
|
|
client_id: "CLIENT002".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// Verify venue was selected
|
|
assert!(!analysis.execution_venue.is_empty(), "Venue should be selected");
|
|
|
|
// Verify alternative venues were evaluated
|
|
assert!(!analysis.alternative_venues.is_empty(),
|
|
"Alternative venues should be evaluated");
|
|
|
|
// Verify venue scores are valid
|
|
for venue in &analysis.alternative_venues {
|
|
assert!(venue.venue_score >= 0.0 && venue.venue_score <= 1.0,
|
|
"Venue score should be between 0 and 1");
|
|
assert!(venue.execution_probability >= 0.0 && venue.execution_probability <= 1.0,
|
|
"Execution probability should be valid percentage");
|
|
}
|
|
}
|
|
|
|
/// Test transaction cost breakdown
|
|
#[tokio::test]
|
|
async fn test_transaction_cost_breakdown() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Sell,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(2000),
|
|
price: Some(Price::from_f64(50.25).expect("Valid price")),
|
|
symbol: "GOOGL".to_string(),
|
|
client_id: "CLIENT003".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
let costs = &analysis.cost_analysis;
|
|
|
|
// Verify explicit costs are calculated
|
|
assert!(costs.explicit_costs.commission >= Decimal::ZERO,
|
|
"Commission should be non-negative");
|
|
assert!(costs.explicit_costs.exchange_fees >= Decimal::ZERO,
|
|
"Exchange fees should be non-negative");
|
|
assert!(costs.explicit_costs.clearing_fees >= Decimal::ZERO,
|
|
"Clearing fees should be non-negative");
|
|
|
|
// Verify implicit costs are calculated
|
|
assert!(costs.implicit_costs.spread_cost_bps >= 0.0,
|
|
"Spread cost should be non-negative");
|
|
assert!(costs.implicit_costs.market_impact_bps >= 0.0,
|
|
"Market impact should be non-negative");
|
|
|
|
// Verify total costs
|
|
assert!(costs.total_costs_bps > 0.0,
|
|
"Total costs should be positive");
|
|
}
|
|
|
|
/// Test best execution compliance assessment
|
|
#[tokio::test]
|
|
async fn test_best_execution_compliance() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(500),
|
|
price: Some(Price::from_f64(150.00).expect("Valid price")),
|
|
symbol: "TSLA".to_string(),
|
|
client_id: "CLIENT004".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// For well-configured system, should be compliant
|
|
assert!(analysis.is_compliant || !analysis.findings.is_empty(),
|
|
"Should either be compliant or have findings explaining non-compliance");
|
|
|
|
// Verify execution score exists and is used for compliance
|
|
assert_eq!(analysis.execution_score, analysis.execution_quality_score,
|
|
"Execution score and quality score should match");
|
|
}
|
|
|
|
/// Test price improvement detection
|
|
#[tokio::test]
|
|
async fn test_price_improvement() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(1000),
|
|
price: Some(Price::from_f64(100.00).expect("Valid price")),
|
|
symbol: "NVDA".to_string(),
|
|
client_id: "CLIENT005".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
let metrics = &analysis.quality_metrics;
|
|
|
|
// Price improvement can be positive (better than NBBO) or negative (worse)
|
|
assert!(metrics.price_improvement_bps.is_finite(),
|
|
"Price improvement should be a valid number");
|
|
|
|
// Verify spread metrics
|
|
assert!(metrics.effective_spread_bps >= 0.0,
|
|
"Effective spread should be non-negative");
|
|
assert!(metrics.realized_spread_bps >= 0.0,
|
|
"Realized spread should be non-negative");
|
|
}
|
|
|
|
/// Test market impact calculation
|
|
#[tokio::test]
|
|
async fn test_market_impact() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
// Large order to test market impact
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: Quantity::from_shares(100000),
|
|
price: None,
|
|
symbol: "AAPL".to_string(),
|
|
client_id: "CLIENT006".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
let metrics = &analysis.quality_metrics;
|
|
|
|
// Large orders should have measurable market impact
|
|
assert!(metrics.market_impact_bps >= 0.0,
|
|
"Market impact should be non-negative");
|
|
}
|
|
|
|
/// Test execution venue types
|
|
#[tokio::test]
|
|
async fn test_venue_types() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(1000),
|
|
price: Some(Price::from_f64(50.00).expect("Valid price")),
|
|
symbol: "AMZN".to_string(),
|
|
client_id: "CLIENT007".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// Verify venue types are properly classified
|
|
for venue in &analysis.alternative_venues {
|
|
match &venue.venue_type {
|
|
VenueType::ReguLatedMarket |
|
|
VenueType::MTF |
|
|
VenueType::OTF |
|
|
VenueType::SystematicInternaliser |
|
|
VenueType::MarketMaker |
|
|
VenueType::OtherLiquidityProvider => {
|
|
// Valid venue type
|
|
assert!(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test custom execution factors configuration
|
|
#[tokio::test]
|
|
async fn test_custom_execution_factors() {
|
|
let custom_config = BestExecutionConfig {
|
|
real_time_monitoring: true,
|
|
execution_factors: ExecutionFactors {
|
|
price_weight: 0.40,
|
|
cost_weight: 0.30,
|
|
speed_weight: 0.15,
|
|
likelihood_weight: 0.10,
|
|
size_weight: 0.03,
|
|
market_impact_weight: 0.02,
|
|
},
|
|
venue_criteria: VenueSelectionCriteria {
|
|
min_volume_threshold: Decimal::from(5000),
|
|
max_latency_tolerance: 500, // 500μs
|
|
min_execution_probability: 0.90,
|
|
max_price_deviation_bps: 10.0,
|
|
},
|
|
reporting_intervals: ReportingIntervals {
|
|
real_time_interval: 1,
|
|
daily_reports: true,
|
|
monthly_rts28_reports: true,
|
|
annual_summary: true,
|
|
},
|
|
min_analysis_period_days: 30,
|
|
};
|
|
|
|
// Verify config is valid
|
|
let total_weight = custom_config.execution_factors.price_weight
|
|
+ custom_config.execution_factors.cost_weight
|
|
+ custom_config.execution_factors.speed_weight
|
|
+ custom_config.execution_factors.likelihood_weight
|
|
+ custom_config.execution_factors.size_weight
|
|
+ custom_config.execution_factors.market_impact_weight;
|
|
|
|
assert!((total_weight - 1.0).abs() < 0.01,
|
|
"Execution factor weights should sum to ~1.0");
|
|
}
|
|
|
|
/// Test execution findings generation
|
|
#[tokio::test]
|
|
async fn test_execution_findings() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(1000),
|
|
price: Some(Price::from_f64(100.00).expect("Valid price")),
|
|
symbol: "META".to_string(),
|
|
client_id: "CLIENT008".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// Findings should be empty for compliant execution or contain valid issues
|
|
for finding in &analysis.findings {
|
|
assert!(!finding.description.is_empty(),
|
|
"Finding description should not be empty");
|
|
assert!(!finding.remedial_action.is_empty(),
|
|
"Remedial action should be specified");
|
|
}
|
|
}
|
|
|
|
/// Test execution documentation
|
|
#[tokio::test]
|
|
async fn test_execution_documentation() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Sell,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(2000),
|
|
price: Some(Price::from_f64(75.50).expect("Valid price")),
|
|
symbol: "NFLX".to_string(),
|
|
client_id: "CLIENT009".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
let docs = &analysis.documentation;
|
|
|
|
// Verify documentation is complete
|
|
assert!(!docs.venue_evaluation.is_empty(),
|
|
"Venue evaluation should be documented");
|
|
assert!(!docs.cost_benefit_analysis.is_empty(),
|
|
"Cost-benefit analysis should be documented");
|
|
assert!(!docs.decision_rationale.is_empty(),
|
|
"Decision rationale should be documented");
|
|
|
|
// Verify market conditions snapshot
|
|
assert!(docs.market_conditions.volatility >= 0.0,
|
|
"Volatility should be non-negative");
|
|
assert!(docs.market_conditions.liquidity_depth >= Decimal::ZERO,
|
|
"Liquidity depth should be non-negative");
|
|
}
|
|
|
|
/// Test high-frequency trading execution quality
|
|
#[tokio::test]
|
|
async fn test_hft_execution_quality() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
// Small HFT order
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: Quantity::from_shares(100),
|
|
price: None,
|
|
symbol: "SPY".to_string(),
|
|
client_id: "HFT_CLIENT".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
let metrics = &analysis.quality_metrics;
|
|
|
|
// HFT orders should have low execution times
|
|
assert!(metrics.avg_execution_time_ms < 1000,
|
|
"HFT execution should be fast (< 1 second)");
|
|
|
|
// High fill rate expected for liquid instruments
|
|
assert!(metrics.fill_rate > 0.90,
|
|
"Fill rate should be high for liquid instruments");
|
|
}
|
|
|
|
/// Test multi-venue execution analysis
|
|
#[tokio::test]
|
|
async fn test_multi_venue_analysis() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(10000),
|
|
price: Some(Price::from_f64(200.00).expect("Valid price")),
|
|
symbol: "GOOG".to_string(),
|
|
client_id: "CLIENT010".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// Should evaluate multiple venues
|
|
assert!(analysis.alternative_venues.len() >= 1,
|
|
"Should evaluate at least one alternative venue");
|
|
|
|
// Verify venues have different characteristics
|
|
let mut venue_ids: Vec<String> = analysis.alternative_venues
|
|
.iter()
|
|
.map(|v| v.venue_id.clone())
|
|
.collect();
|
|
venue_ids.sort();
|
|
venue_ids.dedup();
|
|
|
|
assert_eq!(venue_ids.len(), analysis.alternative_venues.len(),
|
|
"Venue IDs should be unique");
|
|
}
|
|
|
|
/// Test cost methodology validation
|
|
#[tokio::test]
|
|
async fn test_cost_methodology() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(1000),
|
|
price: Some(Price::from_f64(100.00).expect("Valid price")),
|
|
symbol: "AMD".to_string(),
|
|
client_id: "CLIENT011".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// Verify cost methodology is documented
|
|
assert!(!analysis.cost_analysis.methodology.is_empty(),
|
|
"Cost methodology should be documented");
|
|
assert!(analysis.cost_analysis.methodology.contains("MiFID II") ||
|
|
analysis.cost_analysis.methodology.contains("RTS 28"),
|
|
"Should reference regulatory requirements");
|
|
}
|
|
|
|
/// Test execution score calculation accuracy
|
|
#[tokio::test]
|
|
async fn test_execution_score_accuracy() {
|
|
let config = MiFIDConfig::default();
|
|
let analyzer = BestExecutionAnalyzer::new(&config);
|
|
|
|
let order_info = OrderInfo {
|
|
order_id: OrderId::new(),
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_shares(1000),
|
|
price: Some(Price::from_f64(100.00).expect("Valid price")),
|
|
symbol: "INTC".to_string(),
|
|
client_id: "CLIENT012".to_string(),
|
|
timestamp: Utc::now(),
|
|
};
|
|
|
|
let analysis = analyzer.analyze_best_execution(&order_info).await
|
|
.expect("Analysis should succeed");
|
|
|
|
// Execution score should reflect quality metrics and costs
|
|
let score = analysis.execution_score;
|
|
let metrics = &analysis.quality_metrics;
|
|
let costs = &analysis.cost_analysis;
|
|
|
|
// High fill rate and low costs should correlate with higher score
|
|
if metrics.fill_rate > 0.95 && costs.total_costs_bps < 10.0 {
|
|
assert!(score > 0.7,
|
|
"Good execution should have high score");
|
|
}
|
|
|
|
// Verify score is normalized
|
|
assert!(score >= 0.0 && score <= 1.0,
|
|
"Score should be between 0 and 1");
|
|
}
|