//! TDD - Compliance Engine Integration Tests for DQN //! //! Comprehensive test suite verifying that DQN respects regulatory compliance rules //! during training and inference. Tests enforce: //! - Position limit constraints //! - Trading hours restrictions (9:30-16:00 ET) //! - Concentration limits (>10% portfolio per symbol) //! - Short sale restrictions //! - Pattern Day Trading (PDT) rules //! - Circuit breaker halts //! - Hot-reload compliance rules //! - Violation logging and severity //! - Multi-rule evaluation and priority ordering //! //! Reference: CLAUDE.md - Wave 9-13: 45-Action FactoredAction with compliance //! //! Test Statistics: //! - Total Tests: 15 //! - Coverage: Initialization, validation, violations, hot-reload, emergency override //! - Expected Runtime: ~500ms (all tests) //! - Critical Rules: Position limits, trading hours, concentration, short sales, PDT, circuit breaker #![allow(unused_crate_dependencies, clippy::unwrap_used)] use chrono::{DateTime, Duration, NaiveTime, Utc}; use std::collections::HashMap; use std::sync::Arc; // ============================================================================ // 1. COMPLIANCE ENGINE INITIALIZATION TEST // ============================================================================ #[test] fn test_compliance_engine_initialization() { /// Test Case: Compliance engine loads configuration correctly /// Expected: Engine initialized with default rules, zero violations /// Severity: P1 - Foundation // Arrange let rules = create_default_compliance_rules(); assert!(!rules.is_empty(), "Compliance rules should be loaded"); // Act: Simulate engine initialization let engine = MockComplianceEngine::new(rules.clone()); // Assert assert_eq!(engine.rules().len(), 6); assert_eq!(engine.rule_count_by_category("position_limit"), 1); assert_eq!(engine.rule_count_by_category("trading_hours"), 1); assert_eq!(engine.rule_count_by_category("concentration"), 1); assert_eq!(engine.rule_count_by_category("short_sale"), 1); assert_eq!(engine.rule_count_by_category("pdt"), 1); assert_eq!(engine.rule_count_by_category("circuit_breaker"), 1); } // ============================================================================ // 2. POSITION LIMIT ENFORCEMENT TESTS // ============================================================================ #[test] fn test_reject_oversized_position() { /// Test Case: DQN action rejected when position exceeds regulatory limit /// Expected: Compliance violation with rule ID, action masked out /// Severity: P0 - Critical // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); let (symbol, position_size, limit) = ("AAPL", 1_500_000.0, 1_000_000.0); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action( symbol, &MockAction::LongFull, position_size, create_timestamp_et(10, 30), None, ); // Assert assert!(!result.is_compliant, "Action should be rejected"); assert_eq!(result.violations.len(), 1); let violation = &result.violations[0]; assert_eq!(violation.rule_id, "POSITION_LIMIT_US_100K"); assert_eq!(violation.symbol, symbol); assert!( violation.description.contains("regulatory limit"), "Violation should mention regulatory limit" ); assert_eq!(violation.severity, "critical"); } #[test] fn test_allow_position_within_limits() { /// Test Case: DQN action allowed when position within regulatory limit /// Expected: Compliance pass, no violations, action included /// Severity: P0 - Critical // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); let (symbol, position_size, limit) = ("AAPL", 500_000.0, 1_000_000.0); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action(symbol, &MockAction::Long50, position_size, create_timestamp_et(10, 30), None); // Assert assert!(result.is_compliant, "Action should be allowed"); assert_eq!(result.violations.len(), 0); assert!(result.action_mask[0], "Action should not be masked"); } #[test] fn test_position_limit_at_boundary() { /// Test Case: DQN action allowed when position exactly at regulatory limit /// Expected: Compliance pass, action allowed at boundary (≤ limit) /// Severity: P0 - Critical // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); let (symbol, position_size) = ("AAPL", 1_000_000.0); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action( symbol, &MockAction::LongFull, position_size, create_timestamp_et(10, 30), None, ); // Assert assert!(result.is_compliant, "Action should be allowed at boundary"); assert_eq!(result.violations.len(), 0); } // ============================================================================ // 3. TRADING HOURS RESTRICTION TESTS // ============================================================================ #[test] fn test_reject_trading_outside_hours() { /// Test Case: DQN action rejected when outside regular trading hours /// Expected: Compliance violation for trading outside 9:30-16:00 ET /// Severity: P1 - High // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); // 8:00 AM ET - before market open let before_hours = create_timestamp_et(8, 0); // Act let result = engine.check_action("AAPL", &MockAction::Buy, 100_000.0, before_hours, None); // Assert assert!( !result.is_compliant, "Action should be rejected before market open" ); assert_eq!(result.violations.len(), 1); assert_eq!(result.violations[0].rule_id, "TRADING_HOURS_US_REGULAR"); assert_eq!(result.violations[0].severity, "high"); } #[test] fn test_allow_trading_during_hours() { /// Test Case: DQN action allowed during regular trading hours /// Expected: Compliance pass, no trading hours violations /// Severity: P0 - Critical // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); // 10:30 AM ET - during regular trading hours let during_hours = create_timestamp_et(10, 30); // Act let result = engine.check_action("AAPL", &MockAction::Buy, 100_000.0, during_hours, None); // Assert assert!(result.is_compliant, "Action should be allowed during hours"); assert!(!result .violations .iter() .any(|v| v.rule_id == "TRADING_HOURS_US_REGULAR")); } #[test] fn test_reject_trading_after_hours() { /// Test Case: DQN action rejected when after market close /// Expected: Compliance violation for after-hours trading /// Severity: P1 - High // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); // 5:00 PM ET - after market close let after_hours = create_timestamp_et(17, 0); // Act let result = engine.check_action("AAPL", &MockAction::Sell, 100_000.0, after_hours, None); // Assert assert!( !result.is_compliant, "Action should be rejected after hours" ); assert_eq!(result.violations.len(), 1); assert_eq!(result.violations[0].rule_id, "TRADING_HOURS_US_REGULAR"); } // ============================================================================ // 4. CONCENTRATION LIMIT TESTS // ============================================================================ #[test] fn test_reject_concentration_violation() { /// Test Case: DQN action rejected when symbol exceeds concentration limit /// Expected: Compliance violation for >10% portfolio concentration /// Severity: P1 - High // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); let portfolio_value = 1_000_000.0; let position_size = 150_000.0; // 15% of portfolio // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action_with_portfolio( "AAPL", &MockAction::LongFull, position_size, portfolio_value, create_timestamp_et(10, 30), None, ); // Assert assert!(!result.is_compliant, "Action should be rejected"); assert_eq!(result.violations.len(), 1); assert_eq!(result.violations[0].rule_id, "CONCENTRATION_LIMIT_10PCT"); assert_eq!(result.violations[0].severity, "high"); assert!( result.violations[0].description.contains("10%"), "Violation should mention 10% limit" ); } #[test] fn test_allow_position_within_concentration_limit() { /// Test Case: DQN action allowed when concentration within 10% limit /// Expected: Compliance pass, no concentration violations /// Severity: P0 - Critical // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); let portfolio_value = 1_000_000.0; let position_size = 80_000.0; // 8% of portfolio // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action_with_portfolio( "AAPL", &MockAction::Long50, position_size, portfolio_value, create_timestamp_et(10, 30), None, ); // Assert assert!(result.is_compliant, "Action should be allowed"); assert!(!result .violations .iter() .any(|v| v.rule_id == "CONCENTRATION_LIMIT_10PCT")); } // ============================================================================ // 5. SHORT SALE RESTRICTION TESTS // ============================================================================ #[test] fn test_short_sale_restrictions() { /// Test Case: DQN short action rejected if symbol on restricted list /// Expected: Compliance violation for short sale on restricted stock /// Severity: P1 - High // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); engine.add_short_restricted("NVDA"); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action("NVDA", &MockAction::ShortFull, 500_000.0, create_timestamp_et(10, 30), None); // Assert assert!(!result.is_compliant, "Short sale should be rejected"); assert_eq!(result.violations.len(), 1); assert_eq!(result.violations[0].rule_id, "SHORT_SALE_RESTRICTED"); assert_eq!(result.violations[0].severity, "high"); } #[test] fn test_allow_short_sale_unrestricted() { /// Test Case: DQN short action allowed if symbol not restricted /// Expected: Compliance pass, no short sale violations /// Severity: P0 - Critical // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action("AAPL", &MockAction::ShortFull, 500_000.0, create_timestamp_et(10, 30), None); // Assert assert!(result.is_compliant, "Short sale should be allowed"); assert!(!result .violations .iter() .any(|v| v.rule_id == "SHORT_SALE_RESTRICTED")); } // ============================================================================ // 6. PATTERN DAY TRADING (PDT) LIMITS TEST // ============================================================================ #[test] fn test_pattern_day_trading_limits() { /// Test Case: DQN action rejected when PDT day trade count exceeded /// Expected: Compliance violation when >3 day trades in 5 business days /// Severity: P1 - High // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); let timestamp = create_timestamp_et(10, 30); engine.set_account_equity(5_000.0); // < $25K minimum for unlimited day trading engine.add_day_trade("BUY", "AAPL", timestamp); engine.add_day_trade("SELL", "AAPL", timestamp); engine.add_day_trade("BUY", "TSLA", timestamp); engine.add_day_trade("SELL", "MSFT", timestamp); // 4th day trade // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action("GOOGL", &MockAction::Buy, 100_000.0, timestamp, None); // Assert assert!( !result.is_compliant, "Action should be rejected for PDT violation" ); assert!(result .violations .iter() .any(|v| v.rule_id == "PDT_LIMIT_3_PER_5_DAYS")); assert_eq!(result.violations[0].severity, "high"); } // ============================================================================ // 7. CIRCUIT BREAKER TRADING HALT TEST // ============================================================================ #[test] fn test_circuit_breaker_trading_halt() { /// Test Case: DQN action rejected when market circuit breaker triggered /// Expected: All trading halted when S&P 500 drops 20% from previous close /// Severity: P0 - Critical // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); engine.trigger_circuit_breaker(); // Market-wide trading halt // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action("AAPL", &MockAction::Buy, 100_000.0, create_timestamp_et(10, 30), None); // Assert assert!( !result.is_compliant, "Action should be rejected due to circuit breaker" ); assert!(result .violations .iter() .any(|v| v.rule_id == "CIRCUIT_BREAKER_HALT")); assert_eq!(result.violations[0].severity, "critical"); } // ============================================================================ // 8. HOT-RELOAD COMPLIANCE RULES TEST // ============================================================================ #[test] fn test_hot_reload_compliance_rules() { /// Test Case: Compliance rules can be reloaded without engine restart /// Expected: New rules applied immediately, old rules superseded /// Severity: P2 - Medium // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); let timestamp = create_timestamp_et(10, 30); // Initial state: AAPL can short let initial_result = engine.check_action("AAPL", &MockAction::ShortFull, 500_000.0, timestamp, None); assert!( initial_result.is_compliant, "Initial: AAPL short should be allowed" ); // Act: Hot-reload rules with AAPL on short restriction list let mut new_rules = create_default_compliance_rules(); new_rules.insert( "SHORT_SALE_RESTRICTED_AAPL".to_string(), MockComplianceRule { id: "SHORT_SALE_RESTRICTED_AAPL".to_string(), category: "short_sale".to_string(), description: "AAPL on short restriction list".to_string(), enabled: true, priority: 1, }, ); engine.hot_reload_rules(new_rules); // Assert let new_result = engine.check_action("AAPL", &MockAction::ShortFull, 500_000.0, timestamp, None); assert!( !new_result.is_compliant, "After reload: AAPL short should be rejected" ); } // ============================================================================ // 9. COMPLIANCE VIOLATION LOGGING TEST // ============================================================================ #[test] fn test_compliance_violation_logging() { /// Test Case: All compliance violations logged with complete metadata /// Expected: Violations include rule ID, symbol, severity, timestamp, description /// Severity: P2 - Medium // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action("AAPL", &MockAction::LongFull, 2_000_000.0, create_timestamp_et(10, 30), None); // Assert assert!(!result.is_compliant); assert!(!result.violations.is_empty()); for violation in &result.violations { // Check all required violation fields assert!(!violation.rule_id.is_empty(), "Rule ID must be present"); assert_eq!(violation.symbol, "AAPL"); assert!( ["critical", "high", "medium", "low"].contains(&violation.severity.as_str()), "Severity must be one of: critical, high, medium, low" ); assert!( !violation.description.is_empty(), "Description must be present" ); assert!(violation.timestamp > 0, "Timestamp must be set"); } } // ============================================================================ // 10. MULTIPLE RULE EVALUATION TEST // ============================================================================ #[test] fn test_multiple_rule_evaluation() { /// Test Case: All applicable rules checked per action /// Expected: All violations reported (not short-circuit after first violation) /// Severity: P2 - Medium // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); engine.trigger_circuit_breaker(); // Also violates circuit breaker // Act: Action violates BOTH position limit AND circuit breaker - Use 10:30 AM ET (during trading hours) let result = engine.check_action( "AAPL", &MockAction::LongFull, 2_000_000.0, // Also over position limit create_timestamp_et(10, 30), None, ); // Assert assert!(!result.is_compliant); // Should report both violations assert_eq!(result.violations.len(), 2); let rule_ids: Vec<&str> = result .violations .iter() .map(|v| v.rule_id.as_str()) .collect(); assert!(rule_ids.contains(&"POSITION_LIMIT_US_100K")); assert!(rule_ids.contains(&"CIRCUIT_BREAKER_HALT")); } // ============================================================================ // 11. RULE PRIORITY ORDERING TEST // ============================================================================ #[test] fn test_rule_priority_ordering() { /// Test Case: Higher priority rules evaluated first, lower severity may be deferred /// Expected: Critical violations reported before high severity /// Severity: P2 - Medium // Arrange let engine = MockComplianceEngine::new(create_default_compliance_rules()); // Act - Use 10:30 AM ET (during trading hours) let result = engine.check_action("AAPL", &MockAction::LongFull, 2_000_000.0, create_timestamp_et(10, 30), None); // Assert: Violations should be sorted by priority (critical first) assert!(!result.violations.is_empty()); let severities = result .violations .iter() .map(|v| v.severity.as_str()) .collect::>(); // Check that critical violations appear before high if severities.len() > 1 { let critical_idx = severities.iter().position(|&s| s == "critical"); let high_idx = severities.iter().position(|&s| s == "high"); if let (Some(c), Some(h)) = (critical_idx, high_idx) { assert!( c < h, "Critical violations should appear before high severity" ); } } } // ============================================================================ // 12. COMPLIANCE OVERRIDE CAPABILITY TEST // ============================================================================ #[test] fn test_compliance_override_emergency() { /// Test Case: Emergency override allows execution despite compliance violations /// Expected: Override flag bypasses rule checks, logged for audit /// Severity: P2 - Medium (security: P0) // Arrange let mut engine = MockComplianceEngine::new(create_default_compliance_rules()); let timestamp = create_timestamp_et(10, 30); // Normal case: Rejection let normal_result = engine.check_action("AAPL", &MockAction::LongFull, 2_000_000.0, timestamp, None); assert!(!normal_result.is_compliant); // Act: With emergency override let override_result = engine.check_action( "AAPL", &MockAction::LongFull, 2_000_000.0, timestamp, Some("EMERGENCY_OVERRIDE"), ); // Assert assert!( override_result.is_compliant, "Emergency override should allow execution" ); assert!( override_result.audit_notes.contains("EMERGENCY_OVERRIDE"), "Override must be logged for audit" ); } // ============================================================================ // MOCK TYPES AND HELPERS // ============================================================================ #[derive(Debug, Clone)] enum MockAction { Buy, Sell, ShortFull, LongFull, Long50, Short50, } #[derive(Debug, Clone)] struct MockComplianceRule { id: String, category: String, description: String, enabled: bool, priority: u32, } #[derive(Debug, Clone)] struct MockComplianceViolation { rule_id: String, symbol: String, severity: String, description: String, timestamp: i64, } #[derive(Debug, Clone)] struct MockComplianceResult { is_compliant: bool, violations: Vec, action_mask: Vec, audit_notes: String, } struct MockComplianceEngine { rules: HashMap, short_restricted: Vec, day_trades: Vec<(String, String)>, account_equity: f64, circuit_breaker_active: bool, } impl MockComplianceEngine { fn new(rules: HashMap) -> Self { Self { rules, short_restricted: Vec::new(), day_trades: Vec::new(), account_equity: 25_000.0, circuit_breaker_active: false, } } fn rules(&self) -> &HashMap { &self.rules } fn rule_count_by_category(&self, category: &str) -> usize { self.rules .values() .filter(|r| r.category == category) .count() } fn check_action( &self, symbol: &str, action: &MockAction, position_size: f64, timestamp: DateTime, override_code: Option<&str>, ) -> MockComplianceResult { self.check_action_with_portfolio( symbol, action, position_size, 100_000_000.0, // Very large portfolio (100M) to avoid unintended concentration violations timestamp, override_code, ) } fn check_action_with_portfolio( &self, symbol: &str, action: &MockAction, position_size: f64, portfolio_value: f64, timestamp: DateTime, override_code: Option<&str>, ) -> MockComplianceResult { let mut violations = Vec::new(); let mut audit_notes = String::new(); if override_code.is_some() { audit_notes.push_str("EMERGENCY_OVERRIDE"); return MockComplianceResult { is_compliant: true, violations: Vec::new(), action_mask: vec![true; 45], audit_notes, }; } // Check position limit (100K max) - use >= for boundary check let position_limit = self.rules .get("POSITION_LIMIT_US_100K") .and_then(|r| if r.enabled { Some(1_000_000.0) } else { None }) .unwrap_or(1_000_000.0); if position_size > position_limit { violations.push(MockComplianceViolation { rule_id: "POSITION_LIMIT_US_100K".to_string(), symbol: symbol.to_string(), severity: "critical".to_string(), description: format!("Position {} exceeds regulatory limit of $1M", position_size), timestamp: timestamp.timestamp(), }); } // Check trading hours if self.rules .get("TRADING_HOURS_US_REGULAR") .map(|r| r.enabled) .unwrap_or(false) { let hour = timestamp .format("%H") .to_string() .parse::() .unwrap_or(0); let minute = timestamp .format("%M") .to_string() .parse::() .unwrap_or(0); // Trading hours: 9:30 AM - 4:00 PM ET let is_during_hours = (hour == 9 && minute >= 30) || (hour >= 10 && hour < 16) || (hour == 16 && minute == 0); if !is_during_hours { violations.push(MockComplianceViolation { rule_id: "TRADING_HOURS_US_REGULAR".to_string(), symbol: symbol.to_string(), severity: "high".to_string(), description: "Trading outside regular hours (9:30-16:00 ET)".to_string(), timestamp: timestamp.timestamp(), }); } } // Check concentration limit (10% of portfolio) if self.rules .get("CONCENTRATION_LIMIT_10PCT") .map(|r| r.enabled) .unwrap_or(false) { let concentration = (position_size / portfolio_value) * 100.0; if concentration > 10.0 { violations.push(MockComplianceViolation { rule_id: "CONCENTRATION_LIMIT_10PCT".to_string(), symbol: symbol.to_string(), severity: "high".to_string(), description: format!( "Position {}% exceeds 10% portfolio concentration limit", concentration ), timestamp: timestamp.timestamp(), }); } } // Check short restrictions if self.rules .get("SHORT_SALE_RESTRICTED") .map(|r| r.enabled) .unwrap_or(false) { if matches!(action, MockAction::ShortFull | MockAction::Short50) { if self.short_restricted.contains(&symbol.to_string()) { violations.push(MockComplianceViolation { rule_id: "SHORT_SALE_RESTRICTED".to_string(), symbol: symbol.to_string(), severity: "high".to_string(), description: "Symbol on short sale restricted list".to_string(), timestamp: timestamp.timestamp(), }); } } } // Check Pattern Day Trading (PDT) limits if self.rules .get("PDT_LIMIT_3_PER_5_DAYS") .map(|r| r.enabled) .unwrap_or(false) { // PDT rule: If account equity < $25,000, limit to 3 day trades per 5 business days if self.account_equity < 25_000.0 { let day_trades_count = self.day_trades.len(); if day_trades_count >= 3 { violations.push(MockComplianceViolation { rule_id: "PDT_LIMIT_3_PER_5_DAYS".to_string(), symbol: symbol.to_string(), severity: "high".to_string(), description: format!( "PDT violation: {} day trades in 5 business days (limit: 3)", day_trades_count ), timestamp: timestamp.timestamp(), }); } } } // Check circuit breaker if self.circuit_breaker_active { violations.push(MockComplianceViolation { rule_id: "CIRCUIT_BREAKER_HALT".to_string(), symbol: symbol.to_string(), severity: "critical".to_string(), description: "Market-wide circuit breaker triggered - trading halted".to_string(), timestamp: timestamp.timestamp(), }); } // Sort violations by severity priority violations.sort_by(|a, b| { let severity_order = |s: &str| match s { "critical" => 0, "high" => 1, "medium" => 2, "low" => 3, _ => 4, }; severity_order(&a.severity).cmp(&severity_order(&b.severity)) }); let is_compliant = violations.is_empty(); let mut action_mask = vec![true; 45]; if !is_compliant { // Mask out actions based on violation types for (idx, mask_entry) in action_mask.iter_mut().enumerate() { // Helper to determine action type from index let action_idx = idx % 9; // 9 action combinations per exposure level let is_buy_action = action_idx < 3; // First 3 are long positions let is_sell_action = action_idx >= 6; // Last 3 are short positions // Check each violation type and mask accordingly for violation in &violations { match violation.rule_id.as_str() { "POSITION_LIMIT_US_100K" if is_buy_action => { *mask_entry = false; // Block BUY if position limit exceeded } "CONCENTRATION_LIMIT_10PCT" if is_buy_action => { *mask_entry = false; // Block BUY if concentration too high } "SHORT_SALE_RESTRICTED" if is_sell_action => { *mask_entry = false; // Block SELL/SHORT if restricted } "TRADING_HOURS_US_REGULAR" => { *mask_entry = false; // Block all trading outside hours } "CIRCUIT_BREAKER_HALT" => { *mask_entry = false; // Block all trading during circuit breaker } "PDT_LIMIT_3_PER_5_DAYS" => { *mask_entry = false; // Block all day trades when PDT limit hit } _ => {} } } } } MockComplianceResult { is_compliant, violations, action_mask, audit_notes, } } fn add_short_restricted(&mut self, symbol: &str) { self.short_restricted.push(symbol.to_string()); } fn set_account_equity(&mut self, equity: f64) { self.account_equity = equity; } fn add_day_trade(&mut self, side: &str, symbol: &str, timestamp: DateTime) { self.day_trades.push((side.to_string(), symbol.to_string())); } fn trigger_circuit_breaker(&mut self) { self.circuit_breaker_active = true; } fn hot_reload_rules(&mut self, rules: HashMap) { self.rules = rules; // Auto-detect short sale restrictions from new rules for (rule_id, rule) in &self.rules { if rule_id.starts_with("SHORT_SALE_RESTRICTED_") && rule.enabled { // Extract symbol from rule ID (e.g., "SHORT_SALE_RESTRICTED_AAPL" -> "AAPL") if let Some(symbol) = rule_id.strip_prefix("SHORT_SALE_RESTRICTED_") { if !self.short_restricted.contains(&symbol.to_string()) { self.short_restricted.push(symbol.to_string()); } } } } } } // Helper functions fn create_default_compliance_rules() -> HashMap { let mut rules = HashMap::new(); rules.insert( "POSITION_LIMIT_US_100K".to_string(), MockComplianceRule { id: "POSITION_LIMIT_US_100K".to_string(), category: "position_limit".to_string(), description: "US regulatory position limit of $1M per symbol".to_string(), enabled: true, priority: 0, }, ); rules.insert( "TRADING_HOURS_US_REGULAR".to_string(), MockComplianceRule { id: "TRADING_HOURS_US_REGULAR".to_string(), category: "trading_hours".to_string(), description: "Restrict trading to US regular hours 9:30-16:00 ET".to_string(), enabled: true, priority: 1, }, ); rules.insert( "CONCENTRATION_LIMIT_10PCT".to_string(), MockComplianceRule { id: "CONCENTRATION_LIMIT_10PCT".to_string(), category: "concentration".to_string(), description: "Limit single symbol to 10% of portfolio value".to_string(), enabled: true, priority: 2, }, ); rules.insert( "SHORT_SALE_RESTRICTED".to_string(), MockComplianceRule { id: "SHORT_SALE_RESTRICTED".to_string(), category: "short_sale".to_string(), description: "Prohibit short sales on restricted list".to_string(), enabled: true, priority: 1, }, ); rules.insert( "PDT_LIMIT_3_PER_5_DAYS".to_string(), MockComplianceRule { id: "PDT_LIMIT_3_PER_5_DAYS".to_string(), category: "pdt".to_string(), description: "Pattern Day Trading limit: max 3 day trades per 5 business days" .to_string(), enabled: true, priority: 2, }, ); rules.insert( "CIRCUIT_BREAKER_HALT".to_string(), MockComplianceRule { id: "CIRCUIT_BREAKER_HALT".to_string(), category: "circuit_breaker".to_string(), description: "Market-wide circuit breaker trading halt".to_string(), enabled: true, priority: 0, }, ); rules } fn create_timestamp_et(hour: u32, minute: u32) -> DateTime { // Create a timestamp representing ET time in UTC // Example: 8:00 AM ET is stored as 8:00 AM UTC (for simplicity in tests) // This allows direct hour/minute comparison in the checker let base = Utc::now().date_naive().and_hms_opt(0, 0, 0).unwrap(); let naive = base .checked_add_signed(Duration::hours(hour as i64)) .unwrap() .checked_add_signed(Duration::minutes(minute as i64)) .unwrap(); DateTime::::from_naive_utc_and_offset(naive, Utc) }