396 lines
14 KiB
Python
396 lines
14 KiB
Python
"""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"
|