518 lines
18 KiB
Python
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"
|