"""Integration tests for DecisionFusion with PortfolioRiskManager. Tests the integration between decision fusion and portfolio risk management to ensure portfolio-level risk checks can modify or block trading decisions. """ import pytest from openclaw.fusion.decision_fusion import ( AgentOpinion, AgentRole, DecisionFusion, FusionConfig, SignalType, ) from openclaw.portfolio.risk import ( PortfolioRiskManager, create_portfolio_risk_manager, ) class TestDecisionFusionWithPortfolioRisk: """Integration tests for DecisionFusion with PortfolioRiskManager.""" def test_fusion_without_risk_manager(self): """Test fusion works without portfolio risk manager.""" fusion = DecisionFusion() fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Technical breakout", ) ) result = fusion.fuse() assert result.symbol == "AAPL" assert result.final_signal == SignalType.BUY assert "risk_validated" not in result.execution_plan or not result.execution_plan["risk_validated"] def test_fusion_with_risk_manager(self): """Test fusion with portfolio risk manager integration.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.20, max_drawdown_pct=0.10, ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Technical breakout", ) ) positions = {} result = fusion.fuse(portfolio_value=100000.0, positions=positions) assert result.symbol == "AAPL" assert result.execution_plan["risk_validated"] is True assert "risk_score" in result.execution_plan def test_risk_manager_blocks_excessive_position(self): """Test that risk manager blocks trade exceeding concentration limit.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.20, # 20% max concentration ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Strong buy signal", ) ) # Already have a large position that would exceed limit positions = {"AAPL": 25000.0} # 25% of 100k portfolio result = fusion.fuse(portfolio_value=100000.0, positions=positions) # Risk manager should block or reduce assert result.execution_plan["action"] == "HOLD" assert result.execution_plan["position_size"] == "blocked" assert any("BLOCKED" in note for note in result.execution_plan["notes"]) def test_risk_manager_allows_valid_trade(self): """Test that risk manager allows trade within limits.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.20, ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Good entry", ) ) # Small position that won't exceed limit positions = {"AAPL": 5000.0} # 5% of 100k result = fusion.fuse(portfolio_value=100000.0, positions=positions) assert result.execution_plan["action"] == "BUY" assert result.execution_plan["position_size"] != "blocked" def test_risk_manager_reduces_position_size(self): """Test that risk manager reduces position size for high risk.""" risk_manager = create_portfolio_risk_manager( portfolio_id="test_portfolio", risk_profile="conservative", ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.7, reasoning="Entry signal", ) ) # Position that would be at warning level positions = {"AAPL": 14000.0} # 14% of 100k, close to 15% limit result = fusion.fuse(portfolio_value=100000.0, positions=positions) # Should reduce position size due to risk assert result.execution_plan["position_size"] == "reduced" def test_risk_alerts_in_execution_plan(self): """Test that risk alerts are included in execution plan.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.10, # Strict 10% limit ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Buy signal", ) ) # Position that exceeds strict limit positions = {"AAPL": 15000.0} # 15% exceeds 10% limit result = fusion.fuse(portfolio_value=100000.0, positions=positions) # Should have risk alerts assert len(result.execution_plan["risk_alerts"]) > 0 alert = result.execution_plan["risk_alerts"][0] assert alert["type"] == "concentration_limit" def test_position_size_limit_in_plan(self): """Test that position size limit is calculated and included.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.20, ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Buy signal", ) ) positions = {"AAPL": 10000.0} # 10% current result = fusion.fuse(portfolio_value=100000.0, positions=positions) # Should have position size limit (20% max - 10% current = 10% available) assert "position_size_limit" in result.execution_plan assert result.execution_plan["position_size_limit"] > 0 def test_hold_signal_no_risk_check(self): """Test that HOLD signals skip risk checks.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.20, ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") # Add conflicting opinions that result in HOLD fusion.add_opinion( AgentOpinion( agent_id="bull", role=AgentRole.BULL_RESEARCHER, signal=SignalType.BUY, confidence=0.5, reasoning="Bullish", ) ) fusion.add_opinion( AgentOpinion( agent_id="bear", role=AgentRole.BEAR_RESEARCHER, signal=SignalType.SELL, confidence=0.5, reasoning="Bearish", ) ) result = fusion.fuse(portfolio_value=100000.0, positions={}) # Should be HOLD assert result.final_signal == SignalType.HOLD # Risk validation still happens but position size is 0 if result.execution_plan.get("risk_validated"): assert result.execution_plan.get("position_size_limit", 0) == 0.0 def test_multiple_symbols_with_different_risk_profiles(self): """Test fusion with different risk profiles for different symbols.""" conservative_manager = create_portfolio_risk_manager( portfolio_id="conservative", risk_profile="conservative", ) aggressive_manager = create_portfolio_risk_manager( portfolio_id="aggressive", risk_profile="aggressive", ) # Test conservative - should be more restrictive fusion1 = DecisionFusion(portfolio_risk_manager=conservative_manager) fusion1.start_fusion("AAPL") fusion1.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Entry", ) ) result1 = fusion1.fuse( portfolio_value=100000.0, positions={"AAPL": 14000.0} # 14% of 100k ) # Test aggressive - should allow more fusion2 = DecisionFusion(portfolio_risk_manager=aggressive_manager) fusion2.start_fusion("AAPL") fusion2.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Entry", ) ) result2 = fusion2.fuse( portfolio_value=100000.0, positions={"AAPL": 14000.0} # 14% of 100k ) # Conservative should be more restrictive assert result1.execution_plan["position_size"] == "reduced" # Aggressive should allow the trade assert result2.execution_plan["position_size"] != "blocked" def test_risk_manager_factory_function(self): """Test using the factory function to create risk manager.""" risk_manager = create_portfolio_risk_manager( portfolio_id="test", risk_profile="moderate", ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.BUY, confidence=0.8, reasoning="Entry", ) ) result = fusion.fuse(portfolio_value=100000.0, positions={}) assert result.execution_plan["risk_validated"] is True class TestRiskOverrideAndPortfolioRisk: """Tests combining risk manager override with portfolio risk.""" def test_risk_manager_override_takes_precedence(self): """Test that risk manager opinion override takes precedence over portfolio risk.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.20, ) config = FusionConfig(enable_risk_override=True) fusion = DecisionFusion(config=config, portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") # Bullish opinions fusion.add_opinion( AgentOpinion( agent_id="market-1", role=AgentRole.MARKET_ANALYST, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Perfect setup", ) ) # Risk manager strongly warns with high confidence fusion.add_opinion( AgentOpinion( agent_id="risk-1", role=AgentRole.RISK_MANAGER, signal=SignalType.STRONG_SELL, confidence=0.9, reasoning="Market crash imminent", ) ) result = fusion.fuse(portfolio_value=100000.0, positions={}) # Risk manager override should result in SELL assert result.final_signal == SignalType.SELL def test_portfolio_risk_blocks_after_agreement(self): """Test portfolio risk can block even after agents agree.""" risk_manager = PortfolioRiskManager( portfolio_id="test_portfolio", max_concentration_pct=0.05, # Very strict 5% limit ) fusion = DecisionFusion(portfolio_risk_manager=risk_manager) fusion.start_fusion("AAPL") # All agents agree to buy for role in [AgentRole.MARKET_ANALYST, AgentRole.FUNDAMENTAL_ANALYST]: fusion.add_opinion( AgentOpinion( agent_id=f"agent-{role.value}", role=role, signal=SignalType.STRONG_BUY, confidence=0.9, reasoning="Strong agreement", ) ) # But portfolio already has large position positions = {"AAPL": 10000.0} # 10% of 100k, exceeds 5% limit result = fusion.fuse(portfolio_value=100000.0, positions=positions) # Agents agreed on BUY, but portfolio risk blocked it assert result.execution_plan["action"] == "HOLD" assert result.execution_plan["position_size"] == "blocked"