stock/tests/unit/test_decision_fusion.py
2026-02-27 03:17:12 +08:00

492 lines
14 KiB
Python

"""Unit tests for decision fusion module.
Tests the DecisionFusion, AgentOpinion, and FusionResult classes.
"""
import pytest
from openclaw.fusion.decision_fusion import (
AgentOpinion,
AgentRole,
DecisionFusion,
FusionConfig,
FusionResult,
SignalType,
WeightedVote,
)
class TestSignalType:
"""Test SignalType enum."""
def test_signal_values(self):
"""Test signal type values."""
assert SignalType.STRONG_BUY.value == 2
assert SignalType.BUY.value == 1
assert SignalType.HOLD.value == 0
assert SignalType.SELL.value == -1
assert SignalType.STRONG_SELL.value == -2
class TestAgentOpinion:
"""Test AgentOpinion dataclass."""
def test_opinion_creation(self):
"""Test creating an agent opinion."""
opinion = AgentOpinion(
agent_id="market-1",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Technical breakout",
factors=["trend", "volume"],
)
assert opinion.agent_id == "market-1"
assert opinion.role == AgentRole.MARKET_ANALYST
assert opinion.signal == SignalType.BUY
assert opinion.confidence == 0.8
def test_confidence_clamping(self):
"""Test that confidence is clamped to 0-1 range."""
opinion_high = AgentOpinion(
agent_id="test",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=1.5,
reasoning="Test",
)
assert opinion_high.confidence == 1.0
opinion_low = AgentOpinion(
agent_id="test",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=-0.5,
reasoning="Test",
)
assert opinion_low.confidence == 0.0
def test_opinion_to_dict(self):
"""Test converting opinion to dictionary."""
opinion = AgentOpinion(
agent_id="fund-1",
role=AgentRole.FUNDAMENTAL_ANALYST,
signal=SignalType.STRONG_BUY,
confidence=0.9,
reasoning="Strong earnings",
)
d = opinion.to_dict()
assert d["agent_id"] == "fund-1"
assert d["role"] == "fundamental_analyst"
assert d["signal"] == "STRONG_BUY"
assert "timestamp" in d
class TestFusionConfig:
"""Test FusionConfig validation."""
def test_default_weights(self):
"""Test default role weights."""
config = FusionConfig()
assert config.role_weights[AgentRole.RISK_MANAGER] == 1.5
assert config.role_weights[AgentRole.FUNDAMENTAL_ANALYST] == 1.2
assert config.role_weights[AgentRole.MARKET_ANALYST] == 1.0
def test_invalid_confidence_threshold(self):
"""Test invalid confidence threshold raises error."""
with pytest.raises(ValueError):
FusionConfig(confidence_threshold=1.5)
def test_invalid_consensus_threshold(self):
"""Test invalid consensus threshold raises error."""
with pytest.raises(ValueError):
FusionConfig(consensus_threshold=-0.5)
class TestDecisionFusion:
"""Test DecisionFusion functionality."""
@pytest.fixture
def fusion(self):
"""Create a decision fusion instance."""
config = FusionConfig()
return DecisionFusion(config)
def test_start_fusion(self, fusion):
"""Test starting fusion process."""
fusion.start_fusion("AAPL")
assert fusion.symbol == "AAPL"
def test_add_opinion(self, fusion):
"""Test adding opinions."""
fusion.start_fusion("AAPL")
opinion = AgentOpinion(
agent_id="market-1",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Breakout",
)
fusion.add_opinion(opinion)
assert len(fusion._current_opinions) == 1
def test_fuse_single_opinion(self, fusion):
"""Test fusion with single opinion."""
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 result.final_confidence > 0
def test_fuse_multiple_opinions(self, fusion):
"""Test fusion with multiple opinions."""
fusion.start_fusion("AAPL")
# Bullish technical
fusion.add_opinion(
AgentOpinion(
agent_id="market-1",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Breakout",
)
)
# Bullish fundamental
fusion.add_opinion(
AgentOpinion(
agent_id="fund-1",
role=AgentRole.FUNDAMENTAL_ANALYST,
signal=SignalType.STRONG_BUY,
confidence=0.9,
reasoning="Strong earnings",
)
)
# Bearish sentiment
fusion.add_opinion(
AgentOpinion(
agent_id="sent-1",
role=AgentRole.SENTIMENT_ANALYST,
signal=SignalType.SELL,
confidence=0.5,
reasoning="Negative news",
)
)
result = fusion.fuse()
# Should be bullish overall due to strong fundamental + technical
assert result.final_signal.value > 0
assert len(result.weighted_votes) == 3
def test_risk_manager_override(self, fusion):
"""Test risk manager override functionality."""
fusion.config.enable_risk_override = True
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
fusion.add_opinion(
AgentOpinion(
agent_id="risk-1",
role=AgentRole.RISK_MANAGER,
signal=SignalType.STRONG_SELL,
confidence=0.9, # High confidence triggers override
reasoning="High volatility, position too large",
)
)
result = fusion.fuse()
# Risk manager should override
assert result.final_signal == SignalType.SELL
def test_consensus_calculation(self, fusion):
"""Test consensus level calculation."""
fusion.start_fusion("AAPL")
# All bullish = high consensus
for i, role in enumerate([AgentRole.MARKET_ANALYST, AgentRole.FUNDAMENTAL_ANALYST]):
fusion.add_opinion(
AgentOpinion(
agent_id=f"agent-{i}",
role=role,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Agreed",
)
)
result = fusion.fuse()
assert result.consensus_level > 0.5
def test_low_confidence_filtering(self, fusion):
"""Test that low confidence opinions are filtered."""
fusion.config.confidence_threshold = 0.5
fusion.start_fusion("AAPL")
# High confidence - included
fusion.add_opinion(
AgentOpinion(
agent_id="high",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Strong signal",
)
)
# Low confidence - filtered out
fusion.add_opinion(
AgentOpinion(
agent_id="low",
role=AgentRole.SENTIMENT_ANALYST,
signal=SignalType.SELL,
confidence=0.2,
reasoning="Weak signal",
)
)
result = fusion.fuse()
# Only high confidence opinion should be in votes
assert len(result.weighted_votes) == 1
def test_supporting_vs_opposing(self, fusion):
"""Test categorization of supporting vs opposing opinions."""
fusion.start_fusion("AAPL")
# Supporting buy
fusion.add_opinion(
AgentOpinion(
agent_id="bull-1",
role=AgentRole.BULL_RESEARCHER,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Growth",
)
)
# Supporting buy
fusion.add_opinion(
AgentOpinion(
agent_id="bull-2",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.STRONG_BUY,
confidence=0.9,
reasoning="Breakout",
)
)
# Opposing
fusion.add_opinion(
AgentOpinion(
agent_id="bear-1",
role=AgentRole.BEAR_RESEARCHER,
signal=SignalType.SELL,
confidence=0.6,
reasoning="Overvalued",
)
)
result = fusion.fuse()
# Should have more supporting than opposing (bullish consensus)
assert len(result.supporting_opinions) >= len(result.opposing_opinions)
class TestWeightedVote:
"""Test WeightedVote dataclass."""
def test_weighted_vote_creation(self):
"""Test creating a weighted vote."""
opinion = AgentOpinion(
agent_id="test",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Test",
)
vote = WeightedVote(
opinion=opinion,
weight=1.2,
weighted_score=0.96,
)
assert vote.weight == 1.2
assert vote.weighted_score == 0.96
class TestFusionResult:
"""Test FusionResult functionality."""
def test_result_creation(self):
"""Test creating fusion result."""
result = FusionResult(
symbol="AAPL",
final_signal=SignalType.BUY,
final_confidence=0.75,
consensus_level=0.8,
)
assert result.symbol == "AAPL"
assert result.final_confidence == 0.75
def test_get_recommendation_text(self):
"""Test human-readable recommendation."""
result = FusionResult(
symbol="AAPL",
final_signal=SignalType.STRONG_BUY,
final_confidence=0.9,
consensus_level=0.8,
)
text = result.get_recommendation_text()
assert "买入" in text or "Buy" in text
def test_result_to_dict(self):
"""Test converting result to dictionary."""
result = FusionResult(
symbol="AAPL",
final_signal=SignalType.BUY,
final_confidence=0.75,
consensus_level=0.8,
)
d = result.to_dict()
assert d["symbol"] == "AAPL"
assert d["final_signal"] == "BUY"
assert "timestamp" in d
class TestDecisionFusionHistory:
"""Test fusion history tracking."""
def test_get_fusion_history(self):
"""Test retrieving fusion history."""
fusion = DecisionFusion()
# Run multiple fusions
for symbol in ["AAPL", "GOOGL"]:
fusion.start_fusion(symbol)
fusion.add_opinion(
AgentOpinion(
agent_id="test",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Test",
)
)
fusion.fuse()
history = fusion.get_fusion_history()
assert len(history) == 2
def test_get_latest_fusion(self):
"""Test retrieving latest fusion for symbol."""
fusion = DecisionFusion()
# Two fusions for AAPL
for _ in range(2):
fusion.start_fusion("AAPL")
fusion.add_opinion(
AgentOpinion(
agent_id="test",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.8,
reasoning="Test",
)
)
fusion.fuse()
latest = fusion.get_latest_fusion("AAPL")
assert latest is not None
assert latest.symbol == "AAPL"
class TestExecutionPlan:
"""Test execution plan generation."""
def test_strong_signal_plan(self):
"""Test execution plan for strong signals."""
fusion = DecisionFusion()
fusion.start_fusion("AAPL")
# Very high confidence and consensus
fusion.add_opinion(
AgentOpinion(
agent_id="fund",
role=AgentRole.FUNDAMENTAL_ANALYST,
signal=SignalType.STRONG_BUY,
confidence=0.95,
reasoning="Exceptional",
)
)
result = fusion.fuse()
assert result.execution_plan["urgency"] == "high"
assert result.execution_plan["position_size"] == "full"
def test_weak_signal_plan(self):
"""Test execution plan for weak signals."""
fusion = DecisionFusion()
fusion.start_fusion("AAPL")
# Conflicting signals with low confidence = reduced position
fusion.add_opinion(
AgentOpinion(
agent_id="market",
role=AgentRole.MARKET_ANALYST,
signal=SignalType.BUY,
confidence=0.4,
reasoning="Weak buy signal",
)
)
fusion.add_opinion(
AgentOpinion(
agent_id="sentiment",
role=AgentRole.SENTIMENT_ANALYST,
signal=SignalType.SELL,
confidence=0.4,
reasoning="Weak sell signal",
)
)
result = fusion.fuse()
# Conflicting low confidence signals should result in reduced position
assert result.execution_plan["position_size"] in ["reduced", "standard"]