//! Comprehensive tests for config/src/structures.rs //! //! This test suite validates: //! - Serialization/deserialization (JSON, YAML) //! - Default implementations //! - Trait implementations (Clone, Debug, PartialEq) //! - Business logic (broker routing, asset classification, commission calculation) //! - Edge cases and validation use config::structures::*; use rust_decimal::Decimal; // ============================================================================ // SECTION 1: Serialization/Deserialization Tests (6 tests) // ============================================================================ #[test] fn test_risk_config_json_serialization() { let risk_config = RiskConfig::default(); // Serialize to JSON let json = serde_json::to_string(&risk_config).expect("Failed to serialize RiskConfig"); assert!(!json.is_empty(), "JSON should not be empty"); assert!(json.contains("max_position_size"), "JSON should contain max_position_size field"); assert!(json.contains("var_confidence_level"), "JSON should contain var_confidence_level field"); // Verify key fields are present assert!(json.contains("circuit_breaker"), "JSON should contain nested circuit_breaker"); assert!(json.contains("position_limits"), "JSON should contain nested position_limits"); assert!(json.contains("asset_classification"), "JSON should contain nested asset_classification"); } #[test] fn test_risk_config_json_deserialization() { let json = r#"{ "max_position_size": "500000", "max_portfolio_exposure": "5000000", "max_concentration_pct": "0.30", "max_daily_loss": "75000", "max_drawdown_pct": "0.20", "stop_loss_threshold": "40000", "var_confidence_level": 0.99, "var_time_horizon": 5, "var_limit_1d": "60000", "var_limit_10d": "180000", "max_order_size": 150000, "max_orders_per_second": 150, "max_notional_per_hour": "15000000", "kelly_fraction_limit": 0.30, "max_kelly_position_size": 0.25, "emergency_stop_threshold": 0.12, "var_config": { "confidence_level": 0.99, "time_horizon_days": 5, "lookback_period_days": 300, "calculation_method": "monte_carlo", "max_var_limit": 120000.0 }, "circuit_breaker": { "enabled": false, "price_move_threshold": 0.08, "halt_duration_seconds": 600 }, "position_limits": { "global_limit": 15000000.0, "max_leverage": 4.0, "max_var_limit": 120000.0 }, "asset_classification": { "asset_type_rules": {}, "default_sectors": {}, "currency_patterns": [], "crypto_patterns": [] } }"#; let deserialized: RiskConfig = serde_json::from_str(json).expect("Failed to deserialize RiskConfig"); // Verify critical fields assert_eq!(deserialized.max_position_size, Decimal::new(500_000, 0)); assert_eq!(deserialized.var_confidence_level, 0.99); assert_eq!(deserialized.max_orders_per_second, 150); assert!(!deserialized.circuit_breaker.enabled, "Circuit breaker should be disabled"); assert_eq!(deserialized.position_limits.max_leverage, 4.0); } #[test] fn test_var_config_yaml_serialization() { let var_config = VarConfig { confidence_level: 0.99, time_horizon_days: 10, lookback_period_days: 500, calculation_method: "monte_carlo".to_string(), max_var_limit: 250_000.0, }; // Serialize to YAML let yaml = serde_yaml::to_string(&var_config).expect("Failed to serialize VarConfig to YAML"); assert!(!yaml.is_empty(), "YAML should not be empty"); assert!(yaml.contains("confidence_level"), "YAML should contain confidence_level"); assert!(yaml.contains("monte_carlo"), "YAML should contain calculation_method value"); assert!(yaml.contains("250000"), "YAML should contain max_var_limit value"); } #[test] fn test_kelly_config_yaml_deserialization() { let yaml = r#" kelly_fraction: 0.35 max_kelly_leverage: 3.0 min_kelly_leverage: 0.2 confidence_threshold: 0.98 lookback_periods: 365 default_position_fraction: 0.03 enabled: false fractional_kelly: 0.6 min_kelly_fraction: 0.02 max_kelly_fraction: 0.6 "#; let deserialized: KellyConfig = serde_yaml::from_str(yaml).expect("Failed to deserialize KellyConfig from YAML"); // Verify all fields assert_eq!(deserialized.kelly_fraction, 0.35); assert_eq!(deserialized.max_kelly_leverage, 3.0); assert_eq!(deserialized.min_kelly_leverage, 0.2); assert_eq!(deserialized.confidence_threshold, 0.98); assert_eq!(deserialized.lookback_periods, 365); assert!(!deserialized.enabled, "Kelly config should be disabled"); } #[test] fn test_broker_config_json_roundtrip() { let original = BrokerConfig::default(); // Serialize and deserialize let json = serde_json::to_string(&original).expect("Failed to serialize BrokerConfig"); let deserialized: BrokerConfig = serde_json::from_str(&json).expect("Failed to deserialize BrokerConfig"); // Verify structural integrity assert_eq!(original.default_broker, deserialized.default_broker); assert_eq!(original.routing_rules.len(), deserialized.routing_rules.len()); assert_eq!(original.commission_rates.len(), deserialized.commission_rates.len()); // Verify commission rates match for (broker, config) in &original.commission_rates { let deser_config = deserialized.commission_rates.get(broker).expect("Broker should exist"); assert_eq!(config.rate_bps, deser_config.rate_bps); assert_eq!(config.min_commission, deser_config.min_commission); } } #[test] fn test_asset_classification_config_serde_with_enums() { let config = AssetClassificationConfig::default(); // Serialize to JSON let json = serde_json::to_string(&config).expect("Failed to serialize AssetClassificationConfig"); // Verify enum values are properly serialized assert!(json.contains("Equities") || json.contains("equities"), "Should contain Equities asset class"); assert!(json.contains("Alternatives") || json.contains("alternatives"), "Should contain Alternatives asset class"); // Deserialize back let deserialized: AssetClassificationConfig = serde_json::from_str(&json).expect("Failed to deserialize"); // Verify symbol mappings match assert_eq!(config.symbol_mappings.len(), deserialized.symbol_mappings.len()); assert_eq!(config.volatility_profiles.len(), deserialized.volatility_profiles.len()); } // ============================================================================ // SECTION 2: Default Implementation Tests (6 tests) // ============================================================================ #[test] fn test_risk_config_default_values() { let risk_config = RiskConfig::default(); // Position and exposure limits assert_eq!(risk_config.max_position_size, Decimal::new(1_000_000, 0)); assert_eq!(risk_config.max_portfolio_exposure, Decimal::new(10_000_000, 0)); assert_eq!(risk_config.max_concentration_pct, Decimal::new(25_i64, 2)); // Loss and drawdown limits assert_eq!(risk_config.max_daily_loss, Decimal::new(100_000, 0)); assert_eq!(risk_config.max_drawdown_pct, Decimal::new(15_i64, 2)); assert_eq!(risk_config.stop_loss_threshold, Decimal::new(50_000, 0)); // VaR configuration assert_eq!(risk_config.var_confidence_level, 0.95); assert_eq!(risk_config.var_time_horizon, 1); assert_eq!(risk_config.var_limit_1d, Decimal::new(50_000, 0)); // Kelly criterion assert_eq!(risk_config.kelly_fraction_limit, 0.25); assert_eq!(risk_config.max_kelly_position_size, 0.20); // Nested configs have defaults assert!(risk_config.circuit_breaker.enabled); assert!(risk_config.var_config.confidence_level > 0.0); } #[test] fn test_var_config_default_values() { let var_config = VarConfig::default(); assert_eq!(var_config.confidence_level, 0.95); assert_eq!(var_config.time_horizon_days, 1); assert_eq!(var_config.lookback_period_days, 252); assert_eq!(var_config.calculation_method, "historical"); assert_eq!(var_config.max_var_limit, 100_000.0); } #[test] fn test_kelly_config_default_values() { let kelly_config = KellyConfig::default(); assert_eq!(kelly_config.kelly_fraction, 0.25); assert_eq!(kelly_config.max_kelly_leverage, 2.0); assert_eq!(kelly_config.min_kelly_leverage, 0.1); assert_eq!(kelly_config.confidence_threshold, 0.95); assert_eq!(kelly_config.lookback_periods, 252); assert_eq!(kelly_config.default_position_fraction, 0.02); assert!(kelly_config.enabled); assert_eq!(kelly_config.fractional_kelly, 0.5); } #[test] fn test_broker_config_default_routing_rules() { let broker_config = BrokerConfig::default(); // Verify default broker assert_eq!(broker_config.default_broker, "IBKR"); // Verify routing rules exist assert_eq!(broker_config.routing_rules.len(), 3, "Should have 3 default routing rules"); // Verify crypto rule (highest priority) let crypto_rule = &broker_config.routing_rules[0]; assert_eq!(crypto_rule.priority, 100); assert!(crypto_rule.symbol_pattern.contains("BTC") || crypto_rule.symbol_pattern.contains("ETH")); assert_eq!(crypto_rule.broker_id, "ICMARKETS"); // Verify commission rates exist assert!(broker_config.commission_rates.contains_key("ICMARKETS")); assert!(broker_config.commission_rates.contains_key("IBKR")); } #[test] fn test_encryption_config_default_secure_settings() { let encryption_config = EncryptionConfig::default(); assert!(!encryption_config.enable_encryption, "Encryption should be disabled by default"); assert_eq!(encryption_config.algorithm, "AES-256-GCM"); assert_eq!(encryption_config.key_rotation_days, 90); assert!(encryption_config.encryption_keys_vault_path.is_none(), "Vault path should be None by default"); assert!(encryption_config.local_key_file.is_none(), "Local key file should be None by default"); } #[test] fn test_tls_config_default_secure_settings() { let tls_config = TlsConfig::default(); assert!(!tls_config.enabled, "TLS should be disabled by default"); assert!(!tls_config.require_client_cert, "Client cert should not be required by default"); assert_eq!(tls_config.protocol_versions, vec!["TLSv1.3"]); assert!(tls_config.cipher_suites.is_empty(), "Cipher suites should use defaults"); // Verify paths are set (from env or defaults) assert!(!tls_config.cert_path.is_empty()); assert!(!tls_config.key_path.is_empty()); } // ============================================================================ // SECTION 3: Clone and Trait Implementation Tests (4 tests) // ============================================================================ #[test] fn test_risk_config_clone_independence() { let original = RiskConfig::default(); let mut cloned = original.clone(); // Modify cloned version cloned.max_position_size = Decimal::new(2_000_000, 0); cloned.var_confidence_level = 0.99; cloned.max_orders_per_second = 200; // Verify original is unchanged assert_eq!(original.max_position_size, Decimal::new(1_000_000, 0)); assert_eq!(original.var_confidence_level, 0.95); assert_eq!(original.max_orders_per_second, 100); // Verify cloned has new values assert_eq!(cloned.max_position_size, Decimal::new(2_000_000, 0)); assert_eq!(cloned.var_confidence_level, 0.99); assert_eq!(cloned.max_orders_per_second, 200); } #[test] fn test_asset_class_enum_partialeq() { let equities1 = AssetClass::Equities; let equities2 = AssetClass::Equities; let alternatives = AssetClass::Alternatives; // Test equality assert_eq!(equities1, equities2); assert_ne!(equities1, alternatives); // Test all variants assert_eq!(AssetClass::Equities, AssetClass::Equities); assert_eq!(AssetClass::FixedIncome, AssetClass::FixedIncome); assert_eq!(AssetClass::Commodities, AssetClass::Commodities); assert_eq!(AssetClass::Currencies, AssetClass::Currencies); assert_eq!(AssetClass::Alternatives, AssetClass::Alternatives); assert_eq!(AssetClass::Derivatives, AssetClass::Derivatives); assert_eq!(AssetClass::Cash, AssetClass::Cash); } #[test] fn test_broker_config_debug_trait() { let broker_config = BrokerConfig::default(); // Test Debug formatting let debug_str = format!("{:?}", broker_config); assert!(!debug_str.is_empty()); assert!(debug_str.contains("BrokerConfig")); assert!(debug_str.contains("default_broker")); assert!(debug_str.contains("routing_rules")); // Verify no sensitive data leak (API keys, credentials) // (Note: This config doesn't have sensitive fields, but pattern is important) } #[test] fn test_nested_struct_clone_deep_copy() { let original = RiskConfig::default(); let mut cloned = original.clone(); // Modify nested struct in clone cloned.circuit_breaker.price_move_threshold = 0.10; cloned.var_config.confidence_level = 0.99; // Verify original nested structs unchanged assert_eq!(original.circuit_breaker.price_move_threshold, 0.05); assert_eq!(original.var_config.confidence_level, 0.95); // Verify clone has modified values assert_eq!(cloned.circuit_breaker.price_move_threshold, 0.10); assert_eq!(cloned.var_config.confidence_level, 0.99); } // ============================================================================ // SECTION 4: Business Logic Tests - Broker Selection (5 tests) // ============================================================================ #[test] fn test_broker_selection_crypto_routing() { let broker_config = BrokerConfig::default(); // Test BTC/ETH routing to ICMARKETS (priority 100) assert_eq!(broker_config.select_broker("BTCUSD", 100_000.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("ETHUSD", 50_000.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("btcusdt", 200_000.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("ethusdt", 75_000.0), "ICMARKETS"); } #[test] fn test_broker_selection_usd_pairs_quantity_based() { let broker_config = BrokerConfig::default(); // Test USD pairs with quantity <= 1M route to ICMARKETS (priority 90) assert_eq!(broker_config.select_broker("EURUSD", 500_000.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("GBPUSD", 999_999.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("EURUSD", 1_000_000.0), "ICMARKETS"); // Test USD pairs with quantity > 1M don't match USD rule (max_quantity is exclusive) // Fall through to catch-all rule (priority 50) which routes to IBKR assert_eq!(broker_config.select_broker("EURUSD", 1_000_001.0), "IBKR"); assert_eq!(broker_config.select_broker("GBPUSD", 2_000_000.0), "IBKR"); } #[test] fn test_broker_selection_default_fallback() { let broker_config = BrokerConfig::default(); // Test symbols that don't match specific rules fall back to default assert_eq!(broker_config.select_broker("AAPL", 100_000.0), "IBKR"); assert_eq!(broker_config.select_broker("MSFT", 50_000.0), "IBKR"); assert_eq!(broker_config.select_broker("TSLA", 200_000.0), "IBKR"); } #[test] fn test_broker_selection_priority_ordering() { let broker_config = BrokerConfig::default(); // BTCUSD matches both crypto rule (100) and USD rule (90) // Should select higher priority (crypto -> ICMARKETS) assert_eq!(broker_config.select_broker("BTCUSD", 500_000.0), "ICMARKETS"); // ETHUSD same scenario assert_eq!(broker_config.select_broker("ETHUSD", 800_000.0), "ICMARKETS"); } #[test] fn test_broker_selection_case_insensitive() { let broker_config = BrokerConfig::default(); // Test case insensitivity assert_eq!(broker_config.select_broker("btcusd", 100_000.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("BTCUSD", 100_000.0), "ICMARKETS"); assert_eq!(broker_config.select_broker("BtCuSd", 100_000.0), "ICMARKETS"); } // ============================================================================ // SECTION 5: Business Logic Tests - Commission Calculation (3 tests) // ============================================================================ #[test] fn test_commission_calculation_icmarkets() { let broker_config = BrokerConfig::default(); // ICMarkets: 0.7 bps, no minimum let notional = 1_000_000.0; let commission = broker_config.calculate_commission("ICMARKETS", notional); // Expected: 1M * 0.00007 = 70.0 assert_eq!(commission, 70.0); // Test small notional (below potential minimum) let small_commission = broker_config.calculate_commission("ICMARKETS", 1000.0); // Allow small floating point error let expected = 0.07; let diff = (small_commission - expected).abs(); assert!(diff < 0.0001, "Commission calculation mismatch: {} vs {}", small_commission, expected); } #[test] fn test_commission_calculation_ibkr_with_minimum() { let broker_config = BrokerConfig::default(); // IBKR: 0.5 bps, $1.0 minimum let notional = 1_000_000.0; let commission = broker_config.calculate_commission("IBKR", notional); // Expected: 1M * 0.00005 = 50.0 (above minimum) assert_eq!(commission, 50.0); // Test small notional (below minimum) let small_notional = 1000.0; // Would be $0.05 let small_commission = broker_config.calculate_commission("IBKR", small_notional); assert_eq!(small_commission, 1.0); // Minimum applied } #[test] fn test_commission_calculation_unknown_broker_fallback() { let broker_config = BrokerConfig::default(); // Unknown broker should use default 1 bps let notional = 1_000_000.0; let commission = broker_config.calculate_commission("UNKNOWN_BROKER", notional); // Expected: 1M * 0.0001 = 100.0 assert_eq!(commission, 100.0); } // ============================================================================ // SECTION 6: Business Logic Tests - Asset Classification (6 tests) // ============================================================================ #[test] fn test_asset_classification_explicit_mappings() { let config = AssetClassificationConfig::default(); // Test explicit equity mappings assert_eq!(config.classify_symbol("AAPL"), AssetClass::Equities); assert_eq!(config.classify_symbol("MSFT"), AssetClass::Equities); assert_eq!(config.classify_symbol("GOOGL"), AssetClass::Equities); // Test explicit crypto mappings assert_eq!(config.classify_symbol("BTC"), AssetClass::Alternatives); assert_eq!(config.classify_symbol("ETH"), AssetClass::Alternatives); assert_eq!(config.classify_symbol("BTCUSD"), AssetClass::Alternatives); } #[test] fn test_asset_classification_pattern_rules() { let config = AssetClassificationConfig::default(); // Test crypto pattern (highest priority 100) assert_eq!(config.classify_symbol("BTCUSDT"), AssetClass::Alternatives); assert_eq!(config.classify_symbol("ETHEUR"), AssetClass::Alternatives); // Test JPY currency pattern (priority 90) assert_eq!(config.classify_symbol("USDJPY"), AssetClass::Currencies); assert_eq!(config.classify_symbol("EURJPY"), AssetClass::Currencies); // Test generic USD currency pattern (priority 80) assert_eq!(config.classify_symbol("EURUSD"), AssetClass::Currencies); assert_eq!(config.classify_symbol("GBPUSD"), AssetClass::Currencies); } #[test] fn test_asset_classification_priority_ordering() { let config = AssetClassificationConfig::default(); // BTCUSD matches both crypto pattern (100) and USD pattern (80) // Should select higher priority (crypto -> Alternatives) assert_eq!(config.classify_symbol("BTCUSD"), AssetClass::Alternatives); // ETHJPY matches both crypto pattern (100) and JPY pattern (90) // Should select higher priority (crypto -> Alternatives) assert_eq!(config.classify_symbol("ETHJPY"), AssetClass::Alternatives); } #[test] fn test_asset_classification_default_fallback() { let config = AssetClassificationConfig::default(); // Unknown symbols should fall back to Cash assert_eq!(config.classify_symbol("UNKNOWN"), AssetClass::Cash); assert_eq!(config.classify_symbol("XYZ123"), AssetClass::Cash); assert_eq!(config.classify_symbol("!@#$"), AssetClass::Cash); } #[test] fn test_volatility_profile_retrieval() { let config = AssetClassificationConfig::default(); // Test equity volatility profile let equity_profile = config.get_volatility_profile("AAPL"); assert_eq!(equity_profile.annual_volatility, 0.25); assert_eq!(equity_profile.max_position_fraction, 0.20); // Test alternatives (crypto) volatility profile let crypto_profile = config.get_volatility_profile("BTC"); assert_eq!(crypto_profile.annual_volatility, 0.80); assert_eq!(crypto_profile.max_position_fraction, 0.08); // Test currency volatility profile let currency_profile = config.get_volatility_profile("EURUSD"); assert_eq!(currency_profile.annual_volatility, 0.15); assert_eq!(currency_profile.max_position_fraction, 0.30); } #[test] fn test_daily_volatility_calculation() { let config = AssetClassificationConfig::default(); // Test daily volatility calculation (annual_vol / sqrt(252)) let daily_vol_aapl = config.get_daily_volatility("AAPL"); let expected_daily_vol = 0.25 / 252.0_f64.sqrt(); // Allow small floating point error let diff = (daily_vol_aapl - expected_daily_vol).abs(); assert!(diff < 0.0001, "Daily volatility calculation mismatch: {} vs {}", daily_vol_aapl, expected_daily_vol); // Test crypto daily volatility let daily_vol_btc = config.get_daily_volatility("BTC"); let expected_btc = 0.80 / 252.0_f64.sqrt(); let diff_btc = (daily_vol_btc - expected_btc).abs(); assert!(diff_btc < 0.0001); } // ============================================================================ // SECTION 7: Edge Cases and Validation (4 tests) // ============================================================================ #[test] fn test_broker_routing_with_empty_symbol() { let broker_config = BrokerConfig::default(); // Empty symbol should fall back to default broker let result = broker_config.select_broker("", 100_000.0); assert_eq!(result, "IBKR"); } #[test] fn test_broker_routing_with_zero_quantity() { let broker_config = BrokerConfig::default(); // Zero quantity should still route correctly let result = broker_config.select_broker("BTCUSD", 0.0); assert_eq!(result, "ICMARKETS"); // Crypto pattern matches } #[test] fn test_asset_classification_with_lowercase_symbols() { let config = AssetClassificationConfig::default(); // Test lowercase symbols are handled correctly assert_eq!(config.classify_symbol("aapl"), AssetClass::Equities); assert_eq!(config.classify_symbol("btc"), AssetClass::Alternatives); assert_eq!(config.classify_symbol("eurusd"), AssetClass::Currencies); } #[test] fn test_risk_config_tuple_extraction() { let config = AssetClassificationConfig::default(); // Test get_risk_config returns correct tuple let (pos_frac, vol_thresh, loss_thresh) = config.get_risk_config("AAPL"); assert_eq!(pos_frac, 0.20); assert_eq!(vol_thresh, 0.025); assert_eq!(loss_thresh, 0.03); // Test crypto config let (crypto_pos, crypto_vol, crypto_loss) = config.get_risk_config("BTC"); assert_eq!(crypto_pos, 0.08); assert_eq!(crypto_vol, 0.15); assert_eq!(crypto_loss, 0.05); } // ============================================================================ // SECTION 8: Comprehensive Struct Coverage (2 tests) // ============================================================================ #[test] fn test_backtesting_configs_complete() { // Test BacktestingStrategyConfig let strategy_config = BacktestingStrategyConfig::default(); assert_eq!(strategy_config.commission_rate, 0.0007); assert_eq!(strategy_config.slippage_rate, 0.0002); assert_eq!(strategy_config.max_position_size, Some(0.2)); assert_eq!(strategy_config.allow_short_selling, Some(false)); // Test BacktestingPerformanceConfig let perf_config = BacktestingPerformanceConfig::default(); assert_eq!(perf_config.risk_free_rate, 0.04); assert_eq!(perf_config.equity_curve_resolution, 1000); assert_eq!(perf_config.enable_advanced_metrics, Some(true)); } #[test] fn test_trading_and_market_data_configs() { // Test TradingConfig let trading_config = TradingConfig::default(); assert_eq!(trading_config.max_order_size, 1_000_000.0); assert_eq!(trading_config.min_order_size, 0.001); assert_eq!(trading_config.max_price_deviation, 0.05); assert!(!trading_config.enable_symbol_validation); assert_eq!(trading_config.max_batch_notional, 10_000_000.0); assert_eq!(trading_config.max_position_var, 50_000.0); // Test MarketDataConfig let market_config = MarketDataConfig::default(); assert_eq!(market_config.host, "localhost"); assert_eq!(market_config.websocket_port, 8080); assert!(!market_config.use_ssl); assert_eq!(market_config.timeout_seconds, 30); }