stock/tests/unit/test_fundamental_analyst.py
ZhangPeng 9aecdd036c Initial commit: OpenClaw Trading - AI多智能体量化交易系统
- 添加项目核心代码和配置
- 添加前端界面 (Next.js)
- 添加单元测试
- 更新 .gitignore 排除缓存和依赖
2026-02-27 03:47:40 +08:00

493 lines
16 KiB
Python

"""Unit tests for FundamentalAnalyst agent.
This module tests the FundamentalAnalyst class including fundamental analysis,
valuation metrics calculation, and report generation.
"""
import asyncio
from unittest.mock import patch
import pytest
from openclaw.agents.base import ActivityType
from openclaw.agents.fundamental_analyst import (
FundamentalAnalyst,
FundamentalReport,
ValuationRecommendation,
)
from openclaw.core.economy import SurvivalStatus
class TestFundamentalAnalystInitialization:
"""Test FundamentalAnalyst initialization."""
def test_default_initialization(self):
"""Test agent with default parameters."""
agent = FundamentalAnalyst(agent_id="fundamental-1", initial_capital=10000.0)
assert agent.agent_id == "fundamental-1"
assert agent.balance == 10000.0
assert agent.skill_level == 0.5
assert agent.decision_cost == 0.10
assert agent._last_report is None
def test_custom_initialization(self):
"""Test agent with custom parameters."""
agent = FundamentalAnalyst(
agent_id="fundamental-2",
initial_capital=5000.0,
skill_level=0.8,
)
assert agent.agent_id == "fundamental-2"
assert agent.balance == 5000.0
assert agent.skill_level == 0.8
def test_inherits_from_base_agent(self):
"""Test that FundamentalAnalyst inherits from BaseAgent."""
from openclaw.agents.base import BaseAgent
agent = FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
assert isinstance(agent, BaseAgent)
class TestDecideActivity:
"""Test decide_activity method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_bankrupt_agent_only_rests(self, agent):
"""Test that bankrupt agent can only rest."""
agent.economic_tracker.balance = 0 # Bankrupt
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.REST
def test_critical_status_prefers_learning(self, agent):
"""Test critical status leads to learning."""
agent.economic_tracker.balance = 3500.0 # Critical
agent.state.skill_level = 0.5
result = asyncio.run(agent.decide_activity())
assert result in [ActivityType.LEARN, ActivityType.PAPER_TRADE]
def test_struggling_status_when_cannot_afford(self, agent):
"""Test struggling status when cannot afford analysis."""
agent.economic_tracker.balance = 0.20 # Very low, can't afford $0.10 with safety buffer
result = asyncio.run(agent.decide_activity())
assert result in [ActivityType.LEARN, ActivityType.PAPER_TRADE, ActivityType.REST]
def test_healthy_status_performs_analysis(self, agent):
"""Test healthy status with sufficient funds performs analysis."""
agent.economic_tracker.balance = 12000.0 # STABLE status (>110%)
result = asyncio.run(agent.decide_activity())
assert result == ActivityType.ANALYZE
class TestAnalyzeFundamentals:
"""Test analyze_fundamentals method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_returns_fundamental_report(self, agent):
"""Test that analyze_fundamentals returns FundamentalReport."""
result = asyncio.run(agent.analyze_fundamentals("AAPL"))
assert isinstance(result, FundamentalReport)
assert result.symbol == "AAPL"
def test_deducts_decision_cost(self, agent):
"""Test that analysis deducts $0.10 decision cost."""
initial_balance = agent.balance
asyncio.run(agent.analyze_fundamentals("AAPL"))
assert agent.balance == initial_balance - 0.10
def test_valuation_metrics_present(self, agent):
"""Test that valuation metrics are present in report."""
result = asyncio.run(agent.analyze_fundamentals("AAPL"))
assert "pe_ratio" in result.valuation_metrics
assert "pb_ratio" in result.valuation_metrics
assert "market_cap" in result.valuation_metrics
def test_profitability_metrics_present(self, agent):
"""Test that profitability metrics are present in report."""
result = asyncio.run(agent.analyze_fundamentals("TSLA"))
assert "roe" in result.profitability_metrics
assert "roa" in result.profitability_metrics
assert "profit_margin" in result.profitability_metrics
def test_growth_metrics_present(self, agent):
"""Test that growth metrics are present in report."""
result = asyncio.run(agent.analyze_fundamentals("MSFT"))
assert "revenue_growth" in result.growth_metrics
assert "earnings_growth" in result.growth_metrics
def test_overall_score_range(self, agent):
"""Test that overall score is in valid range (0-100)."""
result = asyncio.run(agent.analyze_fundamentals("GOOGL"))
assert 0 <= result.overall_score <= 100
def test_recommendation_valid(self, agent):
"""Test that recommendation is valid."""
result = asyncio.run(agent.analyze_fundamentals("AMZN"))
assert result.recommendation in [
ValuationRecommendation.UNDERVALUED,
ValuationRecommendation.FAIR,
ValuationRecommendation.OVERVALUED,
]
def test_stores_last_report(self, agent):
"""Test that analysis stores the last report."""
assert agent.get_last_report() is None
asyncio.run(agent.analyze_fundamentals("NVDA"))
assert agent.get_last_report() is not None
assert agent.get_last_report().symbol == "NVDA"
def test_timestamp_auto_generated(self, agent):
"""Test that timestamp is auto-generated."""
result = asyncio.run(agent.analyze_fundamentals("META"))
assert result.timestamp != ""
assert "T" in result.timestamp # ISO format
class TestCalculateValuationMetrics:
"""Test _calculate_valuation_metrics method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_calculates_pe_ratio(self, agent):
"""Test PE ratio calculation."""
data = {"price": 100.0, "eps": 5.0, "book_value": 50.0, "revenue": 1e9}
metrics = agent._calculate_valuation_metrics(data)
assert metrics["pe_ratio"] == 20.0 # 100 / 5
def test_calculates_pb_ratio(self, agent):
"""Test PB ratio calculation."""
data = {"price": 100.0, "eps": 5.0, "book_value": 50.0, "revenue": 1e9}
metrics = agent._calculate_valuation_metrics(data)
assert metrics["pb_ratio"] == 2.0 # 100 / 50
def test_handles_zero_values(self, agent):
"""Test handling of zero values."""
data = {"price": 100.0, "eps": 0, "book_value": 0, "revenue": 1e9}
metrics = agent._calculate_valuation_metrics(data)
assert metrics["pe_ratio"] == float("inf")
assert metrics["pb_ratio"] == float("inf")
class TestCalculateProfitabilityMetrics:
"""Test _calculate_profitability_metrics method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_calculates_roe(self, agent):
"""Test ROE calculation."""
data = {
"net_income": 1e9,
"shareholders_equity": 5e9,
"total_assets": 10e9,
"revenue": 10e9,
}
metrics = agent._calculate_profitability_metrics(data)
assert metrics["roe"] == 0.2 # 1e9 / 5e9
def test_calculates_roa(self, agent):
"""Test ROA calculation."""
data = {
"net_income": 1e9,
"shareholders_equity": 5e9,
"total_assets": 10e9,
"revenue": 10e9,
}
metrics = agent._calculate_profitability_metrics(data)
assert metrics["roa"] == 0.1 # 1e9 / 10e9
def test_calculates_profit_margin(self, agent):
"""Test profit margin calculation."""
data = {
"net_income": 2e9,
"shareholders_equity": 5e9,
"total_assets": 10e9,
"revenue": 10e9,
}
metrics = agent._calculate_profitability_metrics(data)
assert metrics["profit_margin"] == 0.2 # 2e9 / 10e9
class TestCalculateGrowthMetrics:
"""Test _calculate_growth_metrics method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_extracts_growth_rates(self, agent):
"""Test growth rate extraction."""
data = {"revenue_growth": 0.15, "earnings_growth": 0.25}
metrics = agent._calculate_growth_metrics(data)
assert metrics["revenue_growth"] == 0.15
assert metrics["earnings_growth"] == 0.25
class TestCalculateOverallScore:
"""Test _calculate_overall_score method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_high_score_for_good_metrics(self, agent):
"""Test high score for strong fundamentals."""
valuation = {"pe_ratio": 10, "pb_ratio": 1.0}
profitability = {"roe": 0.20, "roa": 0.10, "profit_margin": 0.25}
growth = {"revenue_growth": 0.15, "earnings_growth": 0.20}
score = agent._calculate_overall_score(valuation, profitability, growth)
assert score >= 70 # Should be high score
def test_low_score_for_poor_metrics(self, agent):
"""Test low score for weak fundamentals."""
valuation = {"pe_ratio": 50, "pb_ratio": 5.0}
profitability = {"roe": 0.03, "roa": 0.02, "profit_margin": 0.03}
growth = {"revenue_growth": -0.05, "earnings_growth": -0.10}
score = agent._calculate_overall_score(valuation, profitability, growth)
assert score <= 50 # Should be low score
def test_score_clamped_to_100(self, agent):
"""Test score doesn't exceed 100."""
valuation = {"pe_ratio": 5, "pb_ratio": 0.5}
profitability = {"roe": 0.50, "roa": 0.30, "profit_margin": 0.50}
growth = {"revenue_growth": 0.50, "earnings_growth": 0.50}
score = agent._calculate_overall_score(valuation, profitability, growth)
assert score <= 100
def test_score_clamped_to_0(self, agent):
"""Test score doesn't go below 0."""
valuation = {"pe_ratio": 100, "pb_ratio": 10.0}
profitability = {"roe": -0.10, "roa": -0.05, "profit_margin": -0.05}
growth = {"revenue_growth": -0.30, "earnings_growth": -0.30}
score = agent._calculate_overall_score(valuation, profitability, growth)
assert score >= 0
class TestGenerateRecommendation:
"""Test _generate_recommendation method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_undervalued_recommendation(self, agent):
"""Test undervalued recommendation for high score and low PE."""
valuation = {"pe_ratio": 10}
rec = agent._generate_recommendation(75, valuation)
assert rec == ValuationRecommendation.UNDERVALUED
def test_overvalued_recommendation(self, agent):
"""Test overvalued recommendation for low score or very high PE."""
valuation = {"pe_ratio": 50}
rec = agent._generate_recommendation(35, valuation)
assert rec == ValuationRecommendation.OVERVALUED
def test_fair_recommendation(self, agent):
"""Test fair recommendation for neutral conditions."""
valuation = {"pe_ratio": 20}
rec = agent._generate_recommendation(55, valuation)
assert rec == ValuationRecommendation.FAIR
class TestAnalyze:
"""Test analyze method (async BaseAgent interface)."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_analyze_returns_dict(self, agent):
"""Test that analyze returns a dictionary."""
result = asyncio.run(agent.analyze("AAPL"))
assert isinstance(result, dict)
assert result["symbol"] == "AAPL"
assert "overall_score" in result
assert "recommendation" in result
assert "valuation_metrics" in result
assert "profitability_metrics" in result
assert "growth_metrics" in result
assert "cost" in result
def test_analyze_deducts_cost(self, agent):
"""Test that analyze deducts decision cost."""
initial_balance = agent.balance
asyncio.run(agent.analyze("AAPL"))
assert agent.balance < initial_balance
def test_analyze_includes_cost_in_result(self, agent):
"""Test that analyze result includes cost."""
result = asyncio.run(agent.analyze("AAPL"))
assert result["cost"] == 0.10
class TestGetLastReport:
"""Test get_last_report method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_returns_none_when_no_analysis(self, agent):
"""Test returns None when no analysis performed."""
assert agent.get_last_report() is None
def test_returns_report_after_analysis(self, agent):
"""Test returns report after analysis."""
asyncio.run(agent.analyze_fundamentals("AAPL"))
report = agent.get_last_report()
assert report is not None
assert report.symbol == "AAPL"
class TestGetReportHistory:
"""Test get_report_history method."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return FundamentalAnalyst(agent_id="test", initial_capital=10000.0)
def test_returns_empty_list_when_no_analysis(self, agent):
"""Test returns empty list when no analysis performed."""
assert agent.get_report_history() == []
def test_returns_list_with_report_after_analysis(self, agent):
"""Test returns list with report after analysis."""
asyncio.run(agent.analyze_fundamentals("AAPL"))
history = agent.get_report_history()
assert len(history) == 1
assert history[0].symbol == "AAPL"
class TestFundamentalReport:
"""Test FundamentalReport dataclass."""
def test_report_creation(self):
"""Test creating a FundamentalReport."""
report = FundamentalReport(
symbol="AAPL",
valuation_metrics={"pe_ratio": 20.0},
profitability_metrics={"roe": 0.15},
growth_metrics={"revenue_growth": 0.10},
overall_score=75.0,
recommendation=ValuationRecommendation.UNDERVALUED,
)
assert report.symbol == "AAPL"
assert report.overall_score == 75.0
assert report.recommendation == ValuationRecommendation.UNDERVALUED
def test_timestamp_auto_generated(self):
"""Test that timestamp is auto-generated."""
report = FundamentalReport(
symbol="AAPL",
valuation_metrics={},
profitability_metrics={},
growth_metrics={},
overall_score=75.0,
recommendation=ValuationRecommendation.FAIR,
)
assert report.timestamp != ""
assert "T" in report.timestamp # ISO format
def test_custom_timestamp(self):
"""Test FundamentalReport with custom timestamp."""
report = FundamentalReport(
symbol="AAPL",
valuation_metrics={},
profitability_metrics={},
growth_metrics={},
overall_score=75.0,
recommendation=ValuationRecommendation.FAIR,
timestamp="2024-01-01T00:00:00",
)
assert report.timestamp == "2024-01-01T00:00:00"
class TestValuationRecommendation:
"""Test ValuationRecommendation enum."""
def test_recommendation_values(self):
"""Test recommendation enum values."""
assert ValuationRecommendation.UNDERVALUED == "undervalued"
assert ValuationRecommendation.FAIR == "fair"
assert ValuationRecommendation.OVERVALUED == "overvalued"