Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade Files updated: - Cargo.lock: Dependency resolution for Tonic 0.14.2 - All build.rs: Updated for tonic-prost-build - Proto files: Regenerated with tonic-prost 0.14 - Examples/tests: Updated for new gRPC API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
647 lines
20 KiB
Rust
647 lines
20 KiB
Rust
#![allow(unused_crate_dependencies)]
|
|
//! Comprehensive trading engine order validation tests
|
|
//! Target: 95%+ coverage for order validation and business logic
|
|
|
|
use rust_decimal::Decimal;
|
|
use std::str::FromStr;
|
|
|
|
#[cfg(test)]
|
|
mod order_validation_tests {
|
|
use super::*;
|
|
|
|
// ========================================================================
|
|
// Order Price Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_price_positive() {
|
|
let price = Decimal::from_str("100.50").unwrap();
|
|
assert!(validate_order_price(price));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_price_zero() {
|
|
let price = Decimal::from_str("0.00").unwrap();
|
|
assert!(!validate_order_price(price));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_price_negative() {
|
|
let price = Decimal::from_str("-10.00").unwrap();
|
|
assert!(!validate_order_price(price));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_price_tick_size() {
|
|
let price = Decimal::from_str("100.55").unwrap();
|
|
let tick_size = Decimal::from_str("0.05").unwrap();
|
|
assert!(validate_price_tick_size(price, tick_size));
|
|
|
|
let invalid_price = Decimal::from_str("100.53").unwrap();
|
|
assert!(!validate_price_tick_size(invalid_price, tick_size));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_price_extreme_values() {
|
|
let very_high = Decimal::from_str("999999.99").unwrap();
|
|
assert!(validate_order_price(very_high));
|
|
|
|
let very_low = Decimal::from_str("0.0001").unwrap();
|
|
assert!(validate_order_price(very_low));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Order Quantity Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_quantity_positive() {
|
|
let quantity = Decimal::from_str("100").unwrap();
|
|
assert!(validate_order_quantity(quantity));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_quantity_zero() {
|
|
let quantity = Decimal::from_str("0").unwrap();
|
|
assert!(!validate_order_quantity(quantity));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_quantity_negative() {
|
|
let quantity = Decimal::from_str("-50").unwrap();
|
|
assert!(!validate_order_quantity(quantity));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_quantity_fractional() {
|
|
let quantity = Decimal::from_str("10.5").unwrap();
|
|
// Some instruments allow fractional shares
|
|
assert!(validate_order_quantity(quantity));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_quantity_limits() {
|
|
let quantity = Decimal::from_str("100").unwrap();
|
|
let min_quantity = Decimal::from_str("10").unwrap();
|
|
let max_quantity = Decimal::from_str("1000").unwrap();
|
|
|
|
assert!(validate_quantity_limits(
|
|
quantity,
|
|
min_quantity,
|
|
max_quantity
|
|
));
|
|
|
|
let too_small = Decimal::from_str("5").unwrap();
|
|
assert!(!validate_quantity_limits(
|
|
too_small,
|
|
min_quantity,
|
|
max_quantity
|
|
));
|
|
|
|
let too_large = Decimal::from_str("5000").unwrap();
|
|
assert!(!validate_quantity_limits(
|
|
too_large,
|
|
min_quantity,
|
|
max_quantity
|
|
));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Order Notional Value Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_calculate_notional_value() {
|
|
let price = Decimal::from_str("100.00").unwrap();
|
|
let quantity = Decimal::from_str("50").unwrap();
|
|
let notional = calculate_notional_value(price, quantity);
|
|
|
|
assert_eq!(notional, Decimal::from_str("5000.00").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_notional_limits() {
|
|
let notional = Decimal::from_str("10000.00").unwrap();
|
|
let min_notional = Decimal::from_str("1000.00").unwrap();
|
|
let max_notional = Decimal::from_str("100000.00").unwrap();
|
|
|
|
assert!(validate_notional_limits(
|
|
notional,
|
|
min_notional,
|
|
max_notional
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_notional_below_minimum() {
|
|
let notional = Decimal::from_str("500.00").unwrap();
|
|
let min_notional = Decimal::from_str("1000.00").unwrap();
|
|
let max_notional = Decimal::from_str("100000.00").unwrap();
|
|
|
|
assert!(!validate_notional_limits(
|
|
notional,
|
|
min_notional,
|
|
max_notional
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_notional_above_maximum() {
|
|
let notional = Decimal::from_str("150000.00").unwrap();
|
|
let min_notional = Decimal::from_str("1000.00").unwrap();
|
|
let max_notional = Decimal::from_str("100000.00").unwrap();
|
|
|
|
assert!(!validate_notional_limits(
|
|
notional,
|
|
min_notional,
|
|
max_notional
|
|
));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Order Side Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_side_buy() {
|
|
assert!(validate_order_side("BUY"));
|
|
assert!(validate_order_side("buy"));
|
|
assert!(validate_order_side("Buy"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_side_sell() {
|
|
assert!(validate_order_side("SELL"));
|
|
assert!(validate_order_side("sell"));
|
|
assert!(validate_order_side("Sell"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_side_invalid() {
|
|
assert!(!validate_order_side("INVALID"));
|
|
assert!(!validate_order_side(""));
|
|
assert!(!validate_order_side("BID"));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Order Type Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_type_market() {
|
|
assert!(validate_order_type("MARKET"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_type_limit() {
|
|
assert!(validate_order_type("LIMIT"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_type_stop() {
|
|
assert!(validate_order_type("STOP"));
|
|
assert!(validate_order_type("STOP_LIMIT"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_type_invalid() {
|
|
assert!(!validate_order_type("INVALID"));
|
|
assert!(!validate_order_type(""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_limit_order_has_price() {
|
|
let order_type = "LIMIT";
|
|
let price = Some(Decimal::from_str("100.00").unwrap());
|
|
|
|
assert!(validate_limit_order_requirements(order_type, price));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_limit_order_missing_price() {
|
|
let order_type = "LIMIT";
|
|
let price = None;
|
|
|
|
assert!(!validate_limit_order_requirements(order_type, price));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_market_order_no_price_required() {
|
|
let order_type = "MARKET";
|
|
let price = None;
|
|
|
|
assert!(validate_market_order_requirements(order_type, price));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Time in Force Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_time_in_force_day() {
|
|
assert!(validate_time_in_force("DAY"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_time_in_force_gtc() {
|
|
assert!(validate_time_in_force("GTC"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_time_in_force_ioc() {
|
|
assert!(validate_time_in_force("IOC"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_time_in_force_fok() {
|
|
assert!(validate_time_in_force("FOK"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_time_in_force_invalid() {
|
|
assert!(!validate_time_in_force("INVALID"));
|
|
assert!(!validate_time_in_force(""));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Symbol Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_symbol_format_valid() {
|
|
assert!(validate_symbol_format("AAPL"));
|
|
assert!(validate_symbol_format("SPY"));
|
|
assert!(validate_symbol_format("MSFT"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_symbol_format_with_exchange() {
|
|
assert!(validate_symbol_format("AAPL.NASDAQ"));
|
|
assert!(validate_symbol_format("SPY.NYSE"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_symbol_format_invalid() {
|
|
assert!(!validate_symbol_format(""));
|
|
assert!(!validate_symbol_format("@#$"));
|
|
assert!(!validate_symbol_format("SYMBOL_WITH_SPACES"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_symbol_exists() {
|
|
let valid_symbols = vec!["AAPL", "MSFT", "SPY"];
|
|
assert!(validate_symbol_exists("AAPL", &valid_symbols));
|
|
assert!(!validate_symbol_exists("INVALID", &valid_symbols));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Market Hours Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_market_hours_during_trading() {
|
|
// Assume 9:30 AM to 4:00 PM EST trading hours
|
|
let order_time = "14:30:00"; // 2:30 PM
|
|
assert!(validate_market_hours(order_time, "09:30:00", "16:00:00"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_market_hours_before_open() {
|
|
let order_time = "09:00:00"; // Before market open
|
|
assert!(!validate_market_hours(order_time, "09:30:00", "16:00:00"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_market_hours_after_close() {
|
|
let order_time = "17:00:00"; // After market close
|
|
assert!(!validate_market_hours(order_time, "09:30:00", "16:00:00"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_market_hours_boundary_cases() {
|
|
let open_time = "09:30:00";
|
|
let close_time = "16:00:00";
|
|
|
|
// At exact open time
|
|
assert!(validate_market_hours("09:30:00", open_time, close_time));
|
|
|
|
// At exact close time
|
|
assert!(validate_market_hours("16:00:00", open_time, close_time));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Order Status Transition Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_status_transition_new_to_filled() {
|
|
assert!(validate_status_transition("NEW", "FILLED"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_status_transition_new_to_cancelled() {
|
|
assert!(validate_status_transition("NEW", "CANCELLED"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_status_transition_filled_to_cancelled() {
|
|
// Cannot cancel a filled order
|
|
assert!(!validate_status_transition("FILLED", "CANCELLED"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_status_transition_cancelled_to_filled() {
|
|
// Cannot fill a cancelled order
|
|
assert!(!validate_status_transition("CANCELLED", "FILLED"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_status_transition_rejected_is_terminal() {
|
|
assert!(!validate_status_transition("REJECTED", "FILLED"));
|
|
assert!(!validate_status_transition("REJECTED", "CANCELLED"));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Position Limit Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_position_limit_within_bounds() {
|
|
let current_position = Decimal::from_str("500").unwrap();
|
|
let order_quantity = Decimal::from_str("300").unwrap();
|
|
let position_limit = Decimal::from_str("1000").unwrap();
|
|
|
|
assert!(validate_position_limit(
|
|
current_position,
|
|
order_quantity,
|
|
position_limit
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_position_limit_exceeds_limit() {
|
|
let current_position = Decimal::from_str("800").unwrap();
|
|
let order_quantity = Decimal::from_str("300").unwrap();
|
|
let position_limit = Decimal::from_str("1000").unwrap();
|
|
|
|
assert!(!validate_position_limit(
|
|
current_position,
|
|
order_quantity,
|
|
position_limit
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_position_limit_exact_limit() {
|
|
let current_position = Decimal::from_str("700").unwrap();
|
|
let order_quantity = Decimal::from_str("300").unwrap();
|
|
let position_limit = Decimal::from_str("1000").unwrap();
|
|
|
|
assert!(validate_position_limit(
|
|
current_position,
|
|
order_quantity,
|
|
position_limit
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_position_limit_short_position() {
|
|
let current_position = Decimal::from_str("-500").unwrap();
|
|
let order_quantity = Decimal::from_str("-300").unwrap(); // Adding to short
|
|
let position_limit = Decimal::from_str("1000").unwrap();
|
|
|
|
// Absolute value should be checked
|
|
let new_position = current_position + order_quantity;
|
|
assert!(new_position.abs() <= position_limit);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Risk Check Validation Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_risk_within_limits() {
|
|
let order_value = Decimal::from_str("10000.00").unwrap();
|
|
let available_capital = Decimal::from_str("100000.00").unwrap();
|
|
let max_order_percentage = Decimal::from_str("0.20").unwrap();
|
|
|
|
assert!(validate_order_risk(
|
|
order_value,
|
|
available_capital,
|
|
max_order_percentage
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_risk_exceeds_limit() {
|
|
let order_value = Decimal::from_str("30000.00").unwrap();
|
|
let available_capital = Decimal::from_str("100000.00").unwrap();
|
|
let max_order_percentage = Decimal::from_str("0.20").unwrap(); // 20% max
|
|
|
|
assert!(!validate_order_risk(
|
|
order_value,
|
|
available_capital,
|
|
max_order_percentage
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_risk_insufficient_capital() {
|
|
let order_value = Decimal::from_str("10000.00").unwrap();
|
|
let available_capital = Decimal::from_str("5000.00").unwrap();
|
|
let max_order_percentage = Decimal::from_str("0.50").unwrap();
|
|
|
|
assert!(!validate_order_risk(
|
|
order_value,
|
|
available_capital,
|
|
max_order_percentage
|
|
));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Duplicate Order Detection Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_no_duplicate_order() {
|
|
let order_id = "ORD123";
|
|
let existing_orders = vec!["ORD456", "ORD789"];
|
|
|
|
assert!(validate_no_duplicate(order_id, &existing_orders));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_duplicate_order_detected() {
|
|
let order_id = "ORD123";
|
|
let existing_orders = vec!["ORD456", "ORD123", "ORD789"];
|
|
|
|
assert!(!validate_no_duplicate(order_id, &existing_orders));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Order Rate Limiting Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_rate_within_limit() {
|
|
let orders_count = 50;
|
|
let time_window_seconds = 60;
|
|
let max_orders_per_minute = 100;
|
|
|
|
assert!(validate_order_rate(
|
|
orders_count,
|
|
time_window_seconds,
|
|
max_orders_per_minute
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_rate_exceeds_limit() {
|
|
let orders_count = 150;
|
|
let time_window_seconds = 60;
|
|
let max_orders_per_minute = 100;
|
|
|
|
assert!(!validate_order_rate(
|
|
orders_count,
|
|
time_window_seconds,
|
|
max_orders_per_minute
|
|
));
|
|
}
|
|
|
|
// ========================================================================
|
|
// Edge Case Tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_validate_order_with_very_small_price() {
|
|
let price = Decimal::from_str("0.0001").unwrap();
|
|
assert!(validate_order_price(price));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_with_very_large_quantity() {
|
|
let quantity = Decimal::from_str("1000000").unwrap();
|
|
assert!(validate_order_quantity(quantity));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_decimal_precision() {
|
|
let price = Decimal::from_str("100.123456").unwrap();
|
|
let max_precision = 2;
|
|
|
|
assert!(!validate_price_precision(price, max_precision));
|
|
|
|
let valid_price = Decimal::from_str("100.12").unwrap();
|
|
assert!(validate_price_precision(valid_price, max_precision));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_order_symbol_case_insensitive() {
|
|
assert!(validate_symbol_format("aapl"));
|
|
assert!(validate_symbol_format("AAPL"));
|
|
assert!(validate_symbol_format("AaPl"));
|
|
}
|
|
}
|
|
|
|
// Helper validation functions
|
|
|
|
fn validate_order_price(price: Decimal) -> bool {
|
|
price > Decimal::ZERO
|
|
}
|
|
|
|
fn validate_price_tick_size(price: Decimal, tick_size: Decimal) -> bool {
|
|
(price % tick_size) == Decimal::ZERO
|
|
}
|
|
|
|
fn validate_order_quantity(quantity: Decimal) -> bool {
|
|
quantity > Decimal::ZERO
|
|
}
|
|
|
|
fn validate_quantity_limits(quantity: Decimal, min: Decimal, max: Decimal) -> bool {
|
|
quantity >= min && quantity <= max
|
|
}
|
|
|
|
fn calculate_notional_value(price: Decimal, quantity: Decimal) -> Decimal {
|
|
price * quantity
|
|
}
|
|
|
|
fn validate_notional_limits(notional: Decimal, min: Decimal, max: Decimal) -> bool {
|
|
notional >= min && notional <= max
|
|
}
|
|
|
|
fn validate_order_side(side: &str) -> bool {
|
|
let upper = side.to_uppercase();
|
|
upper == "BUY" || upper == "SELL"
|
|
}
|
|
|
|
fn validate_order_type(order_type: &str) -> bool {
|
|
let upper = order_type.to_uppercase();
|
|
matches!(upper.as_str(), "MARKET" | "LIMIT" | "STOP" | "STOP_LIMIT")
|
|
}
|
|
|
|
fn validate_limit_order_requirements(order_type: &str, price: Option<Decimal>) -> bool {
|
|
if order_type.to_uppercase() == "LIMIT" {
|
|
price.is_some()
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
fn validate_market_order_requirements(order_type: &str, _price: Option<Decimal>) -> bool {
|
|
order_type.to_uppercase() == "MARKET"
|
|
}
|
|
|
|
fn validate_time_in_force(tif: &str) -> bool {
|
|
let upper = tif.to_uppercase();
|
|
matches!(upper.as_str(), "DAY" | "GTC" | "IOC" | "FOK")
|
|
}
|
|
|
|
fn validate_symbol_format(symbol: &str) -> bool {
|
|
!symbol.is_empty() && symbol.chars().all(|c| c.is_alphanumeric() || c == '.')
|
|
}
|
|
|
|
fn validate_symbol_exists(symbol: &str, valid_symbols: &[&str]) -> bool {
|
|
valid_symbols.contains(&symbol)
|
|
}
|
|
|
|
fn validate_market_hours(order_time: &str, open: &str, close: &str) -> bool {
|
|
order_time >= open && order_time <= close
|
|
}
|
|
|
|
fn validate_status_transition(from: &str, to: &str) -> bool {
|
|
let valid_transitions = vec![
|
|
("NEW", "FILLED"),
|
|
("NEW", "PARTIALLY_FILLED"),
|
|
("NEW", "CANCELLED"),
|
|
("NEW", "REJECTED"),
|
|
("PARTIALLY_FILLED", "FILLED"),
|
|
("PARTIALLY_FILLED", "CANCELLED"),
|
|
];
|
|
|
|
valid_transitions.contains(&(from, to))
|
|
}
|
|
|
|
fn validate_position_limit(current: Decimal, order_qty: Decimal, limit: Decimal) -> bool {
|
|
(current + order_qty).abs() <= limit
|
|
}
|
|
|
|
fn validate_order_risk(order_value: Decimal, capital: Decimal, max_pct: Decimal) -> bool {
|
|
order_value <= capital && order_value <= capital * max_pct
|
|
}
|
|
|
|
fn validate_no_duplicate(order_id: &str, existing: &[&str]) -> bool {
|
|
!existing.contains(&order_id)
|
|
}
|
|
|
|
fn validate_order_rate(count: usize, window: usize, max_per_window: usize) -> bool {
|
|
if window == 0 {
|
|
return false;
|
|
}
|
|
count <= max_per_window
|
|
}
|
|
|
|
fn validate_price_precision(price: Decimal, max_decimal_places: u32) -> bool {
|
|
let scale = price.scale();
|
|
scale <= max_decimal_places
|
|
}
|