Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
453 lines
13 KiB
Rust
453 lines
13 KiB
Rust
//! Risk Management Validation Tests
|
|
//!
|
|
//! Simplified tests for the risk management system focusing on basic functionality:
|
|
//! - Component creation and initialization
|
|
//! - Basic data structures and type conversions
|
|
//! - Risk violation types and severity levels
|
|
//!
|
|
//! These tests verify the risk system can be instantiated and basic operations work.
|
|
|
|
#![allow(unused_crate_dependencies)]
|
|
|
|
use chrono::Utc;
|
|
use rust_decimal::prelude::FromStr;
|
|
use rust_decimal::Decimal;
|
|
use uuid::Uuid;
|
|
|
|
// Common types from foxhunt ecosystem
|
|
use common::{OrderSide, OrderType, Price, Quantity, Symbol};
|
|
|
|
// Risk crate imports - use actual exported types
|
|
use risk::risk_types::{KillSwitchScope, OrderInfo, RiskViolation, ViolationType};
|
|
use risk::var_calculator::var_engine::RealVaREngine;
|
|
|
|
// Test configuration constants
|
|
const TEST_SYMBOL: &str = "EURUSD";
|
|
const TEST_PORTFOLIO_ID: &str = "test_portfolio_001";
|
|
|
|
// ========== Helper Functions ==========
|
|
|
|
/// Create a test order for risk validation
|
|
fn create_test_order(symbol: &str, quantity: f64, price: f64) -> OrderInfo {
|
|
use std::convert::TryInto;
|
|
|
|
let quantity_decimal = Decimal::try_from(quantity).unwrap_or(Decimal::ZERO);
|
|
let price_decimal = Decimal::try_from(price).unwrap_or(Decimal::ZERO);
|
|
|
|
OrderInfo {
|
|
order_id: format!(
|
|
"test_order_{}",
|
|
Utc::now().timestamp_nanos_opt().unwrap_or(0)
|
|
),
|
|
symbol: Symbol::from(symbol),
|
|
instrument_id: format!("inst_{}", symbol),
|
|
side: OrderSide::Buy,
|
|
quantity: quantity_decimal
|
|
.try_into()
|
|
.unwrap_or(common::Quantity::ZERO),
|
|
price: price_decimal.into(),
|
|
order_type: Some(OrderType::Market),
|
|
portfolio_id: Some(TEST_PORTFOLIO_ID.to_string()),
|
|
strategy_id: None,
|
|
account_id: None,
|
|
}
|
|
}
|
|
|
|
// ========== VaR Calculator Tests ==========
|
|
|
|
#[test]
|
|
fn test_var_engine_creation() {
|
|
// Test that VaR engine can be created
|
|
let var_engine = RealVaREngine::new();
|
|
// Successful creation is the test
|
|
drop(var_engine);
|
|
}
|
|
|
|
#[test]
|
|
fn test_var_engine_multiple_instances() {
|
|
// Test multiple VaR engines can coexist
|
|
let engine1 = RealVaREngine::new();
|
|
let engine2 = RealVaREngine::new();
|
|
let engine3 = RealVaREngine::new();
|
|
|
|
drop(engine1);
|
|
drop(engine2);
|
|
drop(engine3);
|
|
}
|
|
|
|
// ========== Kill Switch Scope Tests ==========
|
|
|
|
#[test]
|
|
fn test_kill_switch_scope_types() {
|
|
// Test all kill switch scope types can be created
|
|
let global = KillSwitchScope::Global;
|
|
let account = KillSwitchScope::Account("test_account".to_string());
|
|
let symbol_str = KillSwitchScope::Symbol("EURUSD".to_string());
|
|
let strategy = KillSwitchScope::Strategy("test_strategy".to_string());
|
|
let portfolio = KillSwitchScope::Portfolio("test_portfolio".to_string());
|
|
|
|
// Test equality
|
|
assert_eq!(global, KillSwitchScope::Global);
|
|
assert_ne!(global, account);
|
|
|
|
drop(symbol_str);
|
|
drop(strategy);
|
|
drop(portfolio);
|
|
}
|
|
|
|
#[test]
|
|
fn test_kill_switch_scope_cloning() {
|
|
let scope = KillSwitchScope::Account("test_account".to_string());
|
|
let cloned = scope.clone();
|
|
|
|
assert_eq!(scope, cloned);
|
|
}
|
|
|
|
// ========== Risk Violation Tests ==========
|
|
|
|
#[test]
|
|
fn test_risk_violation_creation() {
|
|
let violation = RiskViolation {
|
|
id: Uuid::new_v4().to_string(),
|
|
violation_type: ViolationType::PositionSizeExceeded,
|
|
severity: risk::risk_types::RiskSeverity::High,
|
|
message: "Position size limit exceeded".to_string(),
|
|
description: "Test violation for unit testing".to_string(),
|
|
current_value: Some(Price::from_f64(100000.0).unwrap()),
|
|
limit_value: Some(Price::from_f64(50000.0).unwrap()),
|
|
instrument_id: Some(TEST_SYMBOL.to_string()),
|
|
portfolio_id: Some(TEST_PORTFOLIO_ID.to_string()),
|
|
strategy_id: None,
|
|
breach_amount: Some(Price::from_f64(50000.0).unwrap()),
|
|
timestamp: Some(Utc::now().timestamp()),
|
|
resolved: false,
|
|
};
|
|
|
|
assert_eq!(
|
|
violation.violation_type,
|
|
ViolationType::PositionSizeExceeded
|
|
);
|
|
assert_eq!(violation.severity, risk::risk_types::RiskSeverity::High);
|
|
assert!(!violation.resolved);
|
|
assert!(violation.current_value.is_some());
|
|
assert!(violation.limit_value.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_violation_types() {
|
|
// Test all violation types can be created
|
|
let violation_types = vec![
|
|
ViolationType::PositionSizeExceeded,
|
|
ViolationType::PositionLimit,
|
|
ViolationType::ConcentrationRisk,
|
|
ViolationType::DrawdownLimit,
|
|
ViolationType::LeverageLimit,
|
|
ViolationType::VarLimit,
|
|
ViolationType::LeverageExceeded,
|
|
ViolationType::LossLimitExceeded,
|
|
ViolationType::DailyLossLimit,
|
|
ViolationType::ExposureLimit,
|
|
ViolationType::RegulatoryViolation,
|
|
ViolationType::RiskModelBreach,
|
|
];
|
|
|
|
// All types should have string representations
|
|
for vtype in violation_types {
|
|
let s = format!("{}", vtype);
|
|
assert!(
|
|
!s.is_empty(),
|
|
"Violation type should have string representation"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_risk_severity_levels() {
|
|
// Test all severity levels
|
|
use risk::risk_types::RiskSeverity;
|
|
|
|
let low = RiskSeverity::Low;
|
|
let medium = RiskSeverity::Medium;
|
|
let high = RiskSeverity::High;
|
|
let critical = RiskSeverity::Critical;
|
|
|
|
// Test ordering
|
|
assert_ne!(low, high);
|
|
assert_ne!(medium, critical);
|
|
|
|
// Test default
|
|
let default_severity = RiskSeverity::default();
|
|
assert_eq!(default_severity, RiskSeverity::Low);
|
|
}
|
|
|
|
// ========== Order Info Tests ==========
|
|
|
|
#[test]
|
|
fn test_order_info_creation() {
|
|
let order = create_test_order(TEST_SYMBOL, 100000.0, 1.1050);
|
|
|
|
assert_eq!(order.symbol, Symbol::from(TEST_SYMBOL));
|
|
assert_eq!(order.side, OrderSide::Buy);
|
|
assert!(order.order_type.is_some());
|
|
assert_eq!(order.order_type.unwrap(), OrderType::Market);
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_info_with_different_sides() {
|
|
// Test Buy order
|
|
let buy_order = create_test_order(TEST_SYMBOL, 100000.0, 1.1050);
|
|
assert_eq!(buy_order.side, OrderSide::Buy);
|
|
|
|
// Test Sell order
|
|
let mut sell_order = create_test_order(TEST_SYMBOL, 100000.0, 1.1050);
|
|
sell_order.side = OrderSide::Sell;
|
|
assert_eq!(sell_order.side, OrderSide::Sell);
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_info_with_missing_fields() {
|
|
let mut order = create_test_order(TEST_SYMBOL, 100000.0, 1.1050);
|
|
|
|
// Remove optional fields
|
|
order.portfolio_id = None;
|
|
order.strategy_id = None;
|
|
|
|
// Should still be valid
|
|
assert!(order.portfolio_id.is_none());
|
|
assert!(order.strategy_id.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_order_info_with_zero_values() {
|
|
let order = create_test_order(TEST_SYMBOL, 0.0, 0.0);
|
|
|
|
assert_eq!(order.quantity, Quantity::ZERO);
|
|
assert_eq!(order.price, Price::ZERO);
|
|
}
|
|
|
|
// ========== Decimal Conversion Tests ==========
|
|
|
|
#[test]
|
|
fn test_decimal_conversions() {
|
|
// Test various decimal conversions used in risk calculations
|
|
let d1 = Decimal::from_str("1.1050").unwrap();
|
|
let d2 = Decimal::from_str("100000").unwrap();
|
|
|
|
assert!(d1 > Decimal::ZERO);
|
|
assert!(d2 > d1);
|
|
|
|
// Test multiplication
|
|
let product = d1 * d2;
|
|
assert!(product > d2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decimal_edge_cases() {
|
|
// Test edge cases
|
|
assert_eq!(Decimal::ZERO, Decimal::from(0));
|
|
assert_eq!(Decimal::ONE, Decimal::from(1));
|
|
|
|
// Test try_from conversions
|
|
let from_f64 = Decimal::try_from(1.1050).unwrap();
|
|
assert!(from_f64 > Decimal::ZERO);
|
|
}
|
|
|
|
// ========== Price Type Tests ==========
|
|
|
|
#[test]
|
|
fn test_price_creation() {
|
|
let price = Price::from_f64(1.1050).unwrap();
|
|
assert!(price > Price::ZERO);
|
|
|
|
let zero_price = Price::ZERO;
|
|
assert_eq!(zero_price, Price::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_comparisons() {
|
|
let p1 = Price::from_f64(1.0).unwrap();
|
|
let p2 = Price::from_f64(2.0).unwrap();
|
|
|
|
assert!(p1 < p2);
|
|
assert!(p2 > p1);
|
|
assert_ne!(p1, p2);
|
|
}
|
|
|
|
// ========== Symbol Tests ==========
|
|
|
|
#[test]
|
|
fn test_symbol_creation() {
|
|
let symbol = Symbol::from("EURUSD");
|
|
assert_eq!(symbol.as_str(), "EURUSD");
|
|
|
|
let symbol2 = Symbol::from(TEST_SYMBOL);
|
|
assert_eq!(symbol, symbol2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_equality() {
|
|
let s1 = Symbol::from("EURUSD");
|
|
let s2 = Symbol::from("EURUSD");
|
|
let s3 = Symbol::from("GBPUSD");
|
|
|
|
assert_eq!(s1, s2);
|
|
assert_ne!(s1, s3);
|
|
}
|
|
|
|
// ========== Type Conversion Integration Tests ==========
|
|
|
|
#[test]
|
|
fn test_order_info_type_conversions() {
|
|
use std::convert::TryInto;
|
|
|
|
// Test that all type conversions in OrderInfo work
|
|
let quantity_f64 = 100000.0;
|
|
let price_f64 = 1.1050;
|
|
|
|
let quantity_decimal = Decimal::try_from(quantity_f64).unwrap();
|
|
let price_decimal = Decimal::try_from(price_f64).unwrap();
|
|
|
|
let quantity: Quantity = quantity_decimal.try_into().unwrap();
|
|
let price: Price = price_decimal.into();
|
|
|
|
let order = OrderInfo {
|
|
order_id: "test_order".to_string(),
|
|
symbol: Symbol::from(TEST_SYMBOL),
|
|
instrument_id: format!("inst_{}", TEST_SYMBOL),
|
|
side: OrderSide::Buy,
|
|
quantity,
|
|
price,
|
|
order_type: Some(OrderType::Market),
|
|
portfolio_id: Some(TEST_PORTFOLIO_ID.to_string()),
|
|
strategy_id: None,
|
|
account_id: None,
|
|
};
|
|
|
|
assert_eq!(order.quantity, quantity);
|
|
assert_eq!(order.price, price);
|
|
}
|
|
|
|
#[test]
|
|
fn test_violation_price_conversions() {
|
|
let current_value = Price::from_f64(100000.0).unwrap();
|
|
let limit_value = Price::from_f64(50000.0).unwrap();
|
|
|
|
let violation = RiskViolation {
|
|
id: Uuid::new_v4().to_string(),
|
|
violation_type: ViolationType::PositionLimit,
|
|
severity: risk::risk_types::RiskSeverity::Medium,
|
|
message: "Test".to_string(),
|
|
description: "Test".to_string(),
|
|
current_value: Some(current_value),
|
|
limit_value: Some(limit_value),
|
|
instrument_id: None,
|
|
portfolio_id: None,
|
|
strategy_id: None,
|
|
breach_amount: Some(current_value - limit_value),
|
|
timestamp: Some(Utc::now().timestamp()),
|
|
resolved: false,
|
|
};
|
|
|
|
assert!(violation.current_value.unwrap() > violation.limit_value.unwrap());
|
|
assert!(violation.breach_amount.is_some());
|
|
}
|
|
|
|
// ========== Multiple Component Tests ==========
|
|
|
|
#[test]
|
|
fn test_multiple_components_coexist() {
|
|
// Test that multiple risk components can coexist
|
|
let var_engine1 = RealVaREngine::new();
|
|
let var_engine2 = RealVaREngine::new();
|
|
|
|
let order1 = create_test_order("EURUSD", 10000.0, 1.1050);
|
|
let order2 = create_test_order("GBPUSD", 15000.0, 1.2650);
|
|
|
|
let scope1 = KillSwitchScope::Global;
|
|
let scope2 = KillSwitchScope::Account("test".to_string());
|
|
|
|
// All components should coexist without conflicts
|
|
assert_ne!(order1.symbol, order2.symbol);
|
|
assert_ne!(scope1, scope2);
|
|
|
|
drop(var_engine1);
|
|
drop(var_engine2);
|
|
}
|
|
|
|
// ========== Performance Baseline Tests ==========
|
|
|
|
#[test]
|
|
fn test_order_creation_performance() {
|
|
// Baseline test - creating orders should be fast
|
|
use std::time::Instant;
|
|
|
|
let start = Instant::now();
|
|
for i in 0..1000 {
|
|
let _order = create_test_order(
|
|
TEST_SYMBOL,
|
|
(i as f64) * 100.0,
|
|
1.1050 + (i as f64) * 0.0001,
|
|
);
|
|
}
|
|
let elapsed = start.elapsed();
|
|
|
|
// Should complete in under 100ms
|
|
assert!(
|
|
elapsed.as_millis() < 100,
|
|
"Order creation too slow: {:?}",
|
|
elapsed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_violation_creation_performance() {
|
|
use std::time::Instant;
|
|
|
|
let start = Instant::now();
|
|
for _ in 0..1000 {
|
|
let _violation = RiskViolation {
|
|
id: Uuid::new_v4().to_string(),
|
|
violation_type: ViolationType::PositionLimit,
|
|
severity: risk::risk_types::RiskSeverity::Medium,
|
|
message: "Test".to_string(),
|
|
description: "Test".to_string(),
|
|
current_value: Some(Price::from_f64(100000.0).unwrap()),
|
|
limit_value: Some(Price::from_f64(50000.0).unwrap()),
|
|
instrument_id: None,
|
|
portfolio_id: None,
|
|
strategy_id: None,
|
|
breach_amount: None,
|
|
timestamp: Some(Utc::now().timestamp()),
|
|
resolved: false,
|
|
};
|
|
}
|
|
let elapsed = start.elapsed();
|
|
|
|
// Should complete in under 100ms
|
|
assert!(
|
|
elapsed.as_millis() < 100,
|
|
"Violation creation too slow: {:?}",
|
|
elapsed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_var_engine_creation_performance() {
|
|
use std::time::Instant;
|
|
|
|
let start = Instant::now();
|
|
let mut engines = Vec::new();
|
|
for _ in 0..100 {
|
|
engines.push(RealVaREngine::new());
|
|
}
|
|
let elapsed = start.elapsed();
|
|
|
|
// Should be able to create 100 engines quickly
|
|
assert!(
|
|
elapsed.as_millis() < 500,
|
|
"VaR engine creation too slow: {:?}",
|
|
elapsed
|
|
);
|
|
assert_eq!(engines.len(), 100);
|
|
}
|