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

518 lines
18 KiB
Python

"""Unit tests for BearResearcher agent.
This module tests the BearResearcher class including:
- BearReport generation
- Risk factor extraction
- Counter-argument generation
- Decision cost deduction
- Conviction level calculation
"""
import asyncio
from typing import Any, Dict
from unittest.mock import MagicMock
import pytest
from openclaw.agents.base import ActivityType
from openclaw.agents.bear_researcher import BearReport, BearResearcher
from openclaw.core.economy import SurvivalStatus
class TestBearReport:
"""Test BearReport dataclass."""
def test_default_creation(self):
"""Test creating BearReport with default values."""
report = BearReport(symbol="AAPL")
assert report.symbol == "AAPL"
assert report.risk_factors == []
assert report.counter_arguments == {}
assert report.downside_target == 0.0
assert report.conviction_level == 0.0
assert report.summary == ""
def test_full_creation(self):
"""Test creating BearReport with all values."""
report = BearReport(
symbol="TSLA",
risk_factors=["High volatility", "Competition"],
counter_arguments={"Growth": "Slowing"},
downside_target=150.0,
conviction_level=0.75,
summary="Bearish on TSLA",
)
assert report.symbol == "TSLA"
assert len(report.risk_factors) == 2
assert report.counter_arguments["Growth"] == "Slowing"
assert report.downside_target == 150.0
assert report.conviction_level == 0.75
assert report.summary == "Bearish on TSLA"
def test_conviction_level_capped(self):
"""Test conviction level is capped between 0 and 1."""
# Above 1.0 should be capped
report_high = BearReport(symbol="AAPL", conviction_level=1.5)
assert report_high.conviction_level == 1.0
# Below 0 should be floored
report_low = BearReport(symbol="AAPL", conviction_level=-0.5)
assert report_low.conviction_level == 0.0
def test_to_dict(self):
"""Test conversion to dictionary."""
report = BearReport(
symbol="AAPL",
risk_factors=["Risk 1"],
counter_arguments={"Bull": "Counter"},
downside_target=100.0,
conviction_level=0.6,
summary="Test summary",
)
data = report.to_dict()
assert data["symbol"] == "AAPL"
assert data["risk_factors"] == ["Risk 1"]
assert data["counter_arguments"] == {"Bull": "Counter"}
assert data["downside_target"] == 100.0
assert data["conviction_level"] == 0.6
assert data["summary"] == "Test summary"
class TestBearResearcherInitialization:
"""Test BearResearcher initialization."""
def test_default_initialization(self):
"""Test agent with default parameters."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
assert agent.agent_id == "bear-1"
assert agent.balance == 10000.0
assert agent.skill_level == 0.5
assert agent.decision_cost == 0.15
assert agent._last_report is None
def test_custom_initialization(self):
"""Test agent with custom skill level."""
agent = BearResearcher(
agent_id="bear-2",
initial_capital=5000.0,
skill_level=0.8,
)
assert agent.agent_id == "bear-2"
assert agent.balance == 5000.0
assert agent.skill_level == 0.8
def test_repr(self):
"""Test string representation."""
agent = BearResearcher(agent_id="bear-test", initial_capital=10000.0)
repr_str = repr(agent)
assert "BearResearcher" in repr_str
assert "bear-test" in repr_str
assert "$0.15" in repr_str or "0.15" in repr_str
class TestDecideActivity:
"""Test decide_activity method."""
def test_bankrupt_agent_rest(self):
"""Test bankrupt agent can only rest."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
agent.economic_tracker.balance = 1000.0 # Below bankruptcy threshold
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.REST
def test_critical_agent_learns(self):
"""Test critical agent focuses on learning."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
agent.economic_tracker.balance = 3500.0 # Critical level
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.LEARN
def test_struggling_agent_paper_trades(self):
"""Test struggling agent does paper trading."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
agent.economic_tracker.balance = 8500.0 # Struggling level
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.PAPER_TRADE
def test_stable_agent_analyzes(self):
"""Test stable agent analyzes."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
agent.economic_tracker.balance = 12000.0 # Stable level
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.ANALYZE
class TestDecisionCost:
"""Test decision cost deduction."""
def test_decision_cost_deducted_in_analyze(self):
"""Test that decision cost is deducted during analysis."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
initial_balance = agent.balance
asyncio.run(agent.analyze("AAPL"))
# Balance should have decreased
assert agent.balance < initial_balance
def test_decision_cost_deducted_in_generate_bear_case(self):
"""Test that decision cost is deducted when generating bear case."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
initial_balance = agent.balance
asyncio.run(agent.generate_bear_case("AAPL"))
# Balance should have decreased
assert agent.balance < initial_balance
def test_cannot_afford_analysis(self):
"""Test behavior when agent cannot afford analysis."""
# Start with balance below decision cost threshold
agent = BearResearcher(agent_id="bear-1", initial_capital=0.10)
report = asyncio.run(agent.generate_bear_case("AAPL"))
# Should return a report with insufficient funds message
assert report.symbol == "AAPL"
assert "insufficient" in report.summary.lower() or report.conviction_level == 0.0
def test_decision_cost_constant(self):
"""Test that decision cost is set to $0.15."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
assert agent.decision_cost == 0.15
class TestGenerateBearCase:
"""Test generate_bear_case method."""
def test_generates_bear_report(self):
"""Test that bear case generation returns BearReport."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
report = asyncio.run(agent.generate_bear_case("AAPL"))
assert isinstance(report, BearReport)
assert report.symbol == "AAPL"
def test_report_contains_risk_factors(self):
"""Test that report contains risk factors."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.8)
report = asyncio.run(agent.generate_bear_case("AAPL"))
assert len(report.risk_factors) > 0
def test_report_contains_counter_arguments(self):
"""Test that report contains counter-arguments."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
report = asyncio.run(agent.generate_bear_case("AAPL"))
assert len(report.counter_arguments) > 0
def test_conviction_level_in_valid_range(self):
"""Test that conviction level is between 0 and 1."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
report = asyncio.run(agent.generate_bear_case("AAPL"))
assert 0.0 <= report.conviction_level <= 1.0
def test_conviction_based_on_skill(self):
"""Test that higher skill leads to higher conviction."""
low_skill_agent = BearResearcher(
agent_id="bear-low", initial_capital=10000.0, skill_level=0.3
)
high_skill_agent = BearResearcher(
agent_id="bear-high", initial_capital=10000.0, skill_level=0.9
)
low_report = asyncio.run(low_skill_agent.generate_bear_case("AAPL"))
high_report = asyncio.run(high_skill_agent.generate_bear_case("AAPL"))
# Higher skill should generally lead to higher conviction
# (may not always be true due to randomness in tests, but should trend this way)
assert high_report.conviction_level >= low_report.conviction_level
def test_with_technical_report(self):
"""Test bear case generation with technical report input."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.8)
# Mock technical report
tech_report = {"support_level": 150.0, "rsi": 75}
report = asyncio.run(agent.generate_bear_case("AAPL", technical_report=tech_report))
assert report.symbol == "AAPL"
assert len(report.risk_factors) > 0
assert len(report.counter_arguments) > 0
def test_with_all_reports(self):
"""Test bear case generation with all report types."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.9)
tech_report = {"support_level": 150.0}
sentiment_report = {"sentiment": "bullish"}
fundamental_report = {"pe_ratio": 30.0}
report = asyncio.run(
agent.generate_bear_case(
"AAPL",
technical_report=tech_report,
sentiment_report=sentiment_report,
fundamental_report=fundamental_report,
)
)
assert report.symbol == "AAPL"
assert len(report.risk_factors) >= 3 # Should have risks from all reports
assert report.conviction_level > 0.4 # Higher conviction with more data
def test_last_report_saved(self):
"""Test that last report is saved."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
assert agent.get_last_report() is None
report = asyncio.run(agent.generate_bear_case("AAPL"))
assert agent.get_last_report() is report
class TestExtractRiskFactors:
"""Test risk factor extraction."""
def test_extract_from_technical_report(self):
"""Test extracting risks from technical report."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0)
tech_report = MagicMock()
risks = agent._extract_risk_factors(tech_report, None, None)
assert len(risks) > 0
# Should contain technical risks
assert any("RSI" in risk or "support" in risk.lower() for risk in risks)
def test_extract_from_sentiment_report(self):
"""Test extracting risks from sentiment report."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0)
sentiment_report = MagicMock()
risks = agent._extract_risk_factors(None, sentiment_report, None)
assert len(risks) > 0
def test_extract_from_fundamental_report(self):
"""Test extracting risks from fundamental report."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0)
fundamental_report = MagicMock()
risks = agent._extract_risk_factors(None, None, fundamental_report)
assert len(risks) > 0
def test_generic_risks_when_no_reports(self):
"""Test generic risks when no reports provided."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.5)
risks = agent._extract_risk_factors(None, None, None)
assert len(risks) > 0
# Should have generic risks
assert any("volatility" in risk.lower() or "uncertainty" in risk.lower() for risk in risks)
class TestGenerateCounterArguments:
"""Test counter-argument generation."""
def test_counters_with_technical(self):
"""Test counter-arguments with technical report."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
tech_report = MagicMock()
counters = agent._generate_counter_arguments(tech_report, None, None)
assert len(counters) > 0
def test_counters_with_fundamental(self):
"""Test counter-arguments with fundamental report."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
fundamental_report = MagicMock()
counters = agent._generate_counter_arguments(None, None, fundamental_report)
assert len(counters) > 0
# Should contain fundamental counters
assert any("growth" in k.lower() or "valuation" in k.lower() for k in counters.keys())
def test_generic_counters_when_no_reports(self):
"""Test generic counter-arguments when no reports."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
counters = agent._generate_counter_arguments(None, None, None)
assert len(counters) > 0
class TestCounterBullishPoint:
"""Test counter_bullish_point method."""
def test_counter_strong_growth(self):
"""Test counter to strong growth argument."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
counter = agent.counter_bullish_point("strong growth")
assert "peaking" in counter.lower() or "growth" in counter.lower()
def test_counter_undervalued(self):
"""Test counter to undervalued argument."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
counter = agent.counter_bullish_point("undervalued")
assert "value trap" in counter.lower() or "value" in counter.lower()
def test_counter_market_leader(self):
"""Test counter to market leader argument."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
counter = agent.counter_bullish_point("market leader")
assert "competition" in counter.lower() or "leader" in counter.lower()
def test_generic_counter_for_unknown(self):
"""Test generic counter for unknown bullish point."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
counter = agent.counter_bullish_point("some random bullish point xyz")
# Should return a generic counter-argument
assert len(counter) > 0
class TestCalculateConviction:
"""Test conviction calculation."""
def test_conviction_based_on_skill(self):
"""Test conviction is influenced by skill level."""
low_skill = BearResearcher(agent_id="bear-low", initial_capital=10000.0, skill_level=0.3)
high_skill = BearResearcher(agent_id="bear-high", initial_capital=10000.0, skill_level=0.9)
low_conviction = low_skill._calculate_conviction([], None, None, None)
high_conviction = high_skill._calculate_conviction([], None, None, None)
# Higher skill should generally lead to higher base conviction
assert high_conviction > low_conviction
def test_conviction_with_more_risks(self):
"""Test conviction increases with more risk factors."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.5)
few_risks = ["Risk 1"]
many_risks = ["Risk 1", "Risk 2", "Risk 3", "Risk 4"]
few_conviction = agent._calculate_conviction(few_risks, None, None, None)
many_conviction = agent._calculate_conviction(many_risks, None, None, None)
# More risks should generally lead to higher conviction
assert many_conviction >= few_conviction
def test_conviction_capped(self):
"""Test conviction is capped at 0.9."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0)
# Create many risk factors and reports to maximize conviction
many_risks = [f"Risk {i}" for i in range(20)]
tech_report = MagicMock()
sentiment_report = MagicMock()
fundamental_report = MagicMock()
conviction = agent._calculate_conviction(
many_risks, tech_report, sentiment_report, fundamental_report
)
# Should be capped at 0.9 for bearish views
assert conviction <= 0.9
class TestGenerateSummary:
"""Test summary generation."""
def test_summary_based_on_conviction(self):
"""Test summary tone changes based on conviction."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
low_summary = agent._generate_summary("AAPL", [], 0.3)
medium_summary = agent._generate_summary("AAPL", [], 0.5)
high_summary = agent._generate_summary("AAPL", [], 0.8)
# Different conviction levels should produce different tones
assert "Mildly cautious" in low_summary or "cautious" in low_summary.lower()
assert "Bearish" in high_summary or "bearish" in high_summary.lower()
def test_summary_includes_symbol(self):
"""Test summary includes the symbol."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
summary = agent._generate_summary("TSLA", ["Risk 1", "Risk 2"], 0.6)
assert "TSLA" in summary
def test_summary_includes_risk_count(self):
"""Test summary includes risk count."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
risks = ["Risk 1", "Risk 2", "Risk 3"]
summary = agent._generate_summary("AAPL", risks, 0.6)
assert "3" in summary or "three" in summary.lower()
class TestAnalyzeMethod:
"""Test the analyze method."""
def test_analyze_returns_dict(self):
"""Test analyze returns a dictionary."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
result = asyncio.run(agent.analyze("AAPL"))
assert isinstance(result, dict)
assert result["symbol"] == "AAPL"
def test_analyze_deducts_cost(self):
"""Test analyze deducts decision cost."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
initial_balance = agent.balance
asyncio.run(agent.analyze("AAPL"))
assert agent.balance < initial_balance
def test_analyze_contains_bear_report(self):
"""Test analyze result contains bear report."""
agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0)
result = asyncio.run(agent.analyze("AAPL"))
assert "bear_report" in result
assert result["bear_report"]["symbol"] == "AAPL"