//! Integration test: Risk limit violation -> Kill switch activation //! //! Verifies that risk safety mechanisms properly trigger under stress, //! correctly scope kill switch activations, and block trading when active. //! This is a critical path test for the Risk -> Kill Switch pipeline. use risk::safety::kill_switch::{AtomicKillSwitch, TradingGate}; use risk::safety::KillSwitchConfig; use risk::risk_types::KillSwitchScope; fn create_test_kill_switch() -> AtomicKillSwitch { let config = KillSwitchConfig::default(); AtomicKillSwitch::new_test(config) } // --------------------------------------------------------------------------- // Test 1: Kill switch activation and trading blocking // --------------------------------------------------------------------------- #[tokio::test] async fn test_kill_switch_activation_and_blocking() { let kill_switch = create_test_kill_switch(); // Initially not triggered assert!( !kill_switch.is_triggered(), "Kill switch should start inactive" ); assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Trading should be allowed initially" ); // Trigger the kill switch via global activation let result = kill_switch .activate_global( "Max drawdown exceeded: -5.2%".to_string(), "risk_monitor".to_string(), ) .await; assert!(result.is_ok(), "Triggering kill switch should succeed"); // Now it should be triggered assert!( kill_switch.is_triggered(), "Kill switch should be active after trigger" ); // Trading should be blocked assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Trading must be blocked after global kill switch trigger" ); // Also blocked for any scoped query (global takes precedence) assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("ES.FUT".to_string())), "Symbol-scoped trading must also be blocked by global kill switch" ); } // --------------------------------------------------------------------------- // Test 2: Scoped kill switch (symbol-level) // --------------------------------------------------------------------------- #[tokio::test] async fn test_scoped_kill_switch_symbol() { let kill_switch = create_test_kill_switch(); // Trigger for a specific symbol scope only let result = kill_switch .engage( KillSwitchScope::Symbol("ES.FUT".to_string()), "Symbol-level risk limit breached".to_string(), "symbol_monitor".to_string(), false, // no cascade ) .await; assert!(result.is_ok(), "Scoped trigger should succeed"); // Global should NOT be triggered assert!( !kill_switch.is_triggered(), "Global kill switch should NOT be triggered by symbol-scoped engagement" ); assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Global trading should still be allowed" ); // The specific symbol should be blocked assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("ES.FUT".to_string())), "ES.FUT trading should be blocked" ); // Other symbols should NOT be blocked assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("NQ.FUT".to_string())), "NQ.FUT trading should still be allowed" ); // Health metrics should be finite let (error_rate, _failures) = kill_switch.get_health_metrics(); assert!( error_rate.is_finite(), "Error rate should be finite, got {}", error_rate ); } // --------------------------------------------------------------------------- // Test 3: Kill switch reset restores trading // --------------------------------------------------------------------------- #[tokio::test] async fn test_kill_switch_reset_restores_trading() { let kill_switch = create_test_kill_switch(); // Trigger globally kill_switch.trigger(); assert!(kill_switch.is_triggered()); assert!(!kill_switch.is_trading_allowed(&KillSwitchScope::Global)); // Reset let result = kill_switch.reset(Some(KillSwitchScope::Global)).await; assert!(result.is_ok(), "Reset should succeed"); // Trading should be restored assert!(!kill_switch.is_triggered(), "Kill switch should be cleared"); assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Trading should be allowed after reset" ); } // --------------------------------------------------------------------------- // Test 4: Multiple scoped activations and selective reset // --------------------------------------------------------------------------- #[tokio::test] async fn test_multiple_scoped_activations_and_selective_reset() { let kill_switch = create_test_kill_switch(); // Engage two different scopes kill_switch .engage( KillSwitchScope::Symbol("AAPL".to_string()), "AAPL halt".to_string(), "user".to_string(), false, ) .await .expect("AAPL engage should succeed"); kill_switch .engage( KillSwitchScope::Account("ACC-001".to_string()), "Account risk limit".to_string(), "user".to_string(), false, ) .await .expect("Account engage should succeed"); // Both should be blocked assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("AAPL".to_string())), "AAPL should be blocked" ); assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Account("ACC-001".to_string())), "ACC-001 should be blocked" ); // Global still allowed assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Global should still be allowed" ); // Reset only the symbol scope kill_switch .reset(Some(KillSwitchScope::Symbol("AAPL".to_string()))) .await .expect("AAPL reset should succeed"); // AAPL should be restored, account still blocked assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("AAPL".to_string())), "AAPL should be allowed after reset" ); assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Account("ACC-001".to_string())), "ACC-001 should remain blocked" ); } // --------------------------------------------------------------------------- // Test 5: Cascade behavior (portfolio -> strategies) // --------------------------------------------------------------------------- #[tokio::test] async fn test_cascade_portfolio_to_strategies() { let kill_switch = create_test_kill_switch(); // Trigger a portfolio-level kill switch with cascade=true kill_switch .engage( KillSwitchScope::Portfolio("portfolio1".to_string()), "Portfolio drawdown exceeded".to_string(), "risk_engine".to_string(), true, // cascade ) .await .expect("Portfolio engage should succeed"); // The portfolio itself should be blocked assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Portfolio("portfolio1".to_string())), "Portfolio should be blocked" ); // The system is active let is_active = kill_switch.is_active().await.expect("is_active should work"); assert!(is_active, "Kill switch should report as active"); } // --------------------------------------------------------------------------- // Test 6: Metrics tracking // --------------------------------------------------------------------------- #[tokio::test] async fn test_kill_switch_metrics_tracking() { let kill_switch = create_test_kill_switch(); // Initial metrics should be zero let (checks_before, commands_before) = kill_switch.get_metrics(); assert_eq!(checks_before, 0, "Initial health checks should be 0"); assert_eq!(commands_before, 0, "Initial commands should be 0"); // Perform operations that increment counters kill_switch .engage( KillSwitchScope::Global, "Test".to_string(), "user".to_string(), false, ) .await .expect("engage should succeed"); kill_switch .reset(Some(KillSwitchScope::Global)) .await .expect("reset should succeed"); // Commands should have incremented (engage + reset = 2) let (_checks_after, commands_after) = kill_switch.get_metrics(); assert_eq!( commands_after, 2, "Two commands (engage + reset) should be tracked, got {}", commands_after ); // Health metrics: no failures (no Redis in test mode) let (error_rate, failures) = kill_switch.get_health_metrics(); assert_eq!(error_rate, 0.0, "Error rate should be 0.0 with no Redis"); assert_eq!(failures, 0, "Failures should be 0 with no Redis"); } // --------------------------------------------------------------------------- // Test 7: Trading gate integration // --------------------------------------------------------------------------- #[tokio::test] async fn test_trading_gate_lifecycle() { let gate = TradingGate::new(true); assert!(gate.is_open(), "Gate should start open"); // Close gate (simulating risk event) gate.close(); assert!(!gate.is_open(), "Gate should be closed"); // Reopen gate (simulating risk clearance) gate.open(); assert!(gate.is_open(), "Gate should be reopened"); } // --------------------------------------------------------------------------- // Test 8: Kill switch health check (no Redis = healthy) // --------------------------------------------------------------------------- #[tokio::test] async fn test_kill_switch_health_in_test_mode() { let kill_switch = create_test_kill_switch(); let healthy = kill_switch .is_healthy() .await .expect("is_healthy should succeed"); assert!( healthy, "Kill switch without Redis should report healthy (test mode)" ); } // --------------------------------------------------------------------------- // Test 9: Deactivate restores scoped trading // --------------------------------------------------------------------------- #[tokio::test] async fn test_deactivate_restores_scoped_trading() { let kill_switch = create_test_kill_switch(); let scope = KillSwitchScope::Strategy("momentum_v2".to_string()); // Activate kill_switch .activate(scope.clone(), "Test halt".to_string(), "user".to_string(), false) .await .expect("activate should succeed"); assert!( !kill_switch.is_trading_allowed(&scope), "Strategy should be blocked after activation" ); // Deactivate kill_switch .deactivate(scope.clone(), "user".to_string()) .await .expect("deactivate should succeed"); assert!( kill_switch.is_trading_allowed(&scope), "Strategy should be allowed after deactivation" ); }