test_shareholder_analyzer.pyβ’14 kB
"""
Unit tests for ShareholderAnalyzer class
TDD Red Phase: Write failing tests for shareholder analysis engine
"""
import pytest
import asyncio
from unittest.mock import Mock, AsyncMock, patch
from datetime import datetime, date, timedelta
from typing import Dict, Any, List, Optional
# Test imports - initially will fail (TDD Red phase)
from src.tools.shareholder_tools import ShareholderAnalyzer
from src.exceptions import MCPStockDetailsError, InsufficientDataError
class TestShareholderAnalyzer:
"""Test cases for ShareholderAnalyzer class"""
@pytest.fixture
def shareholder_analyzer(self):
"""Create ShareholderAnalyzer instance for testing"""
return ShareholderAnalyzer()
@pytest.fixture
def sample_shareholder_data(self):
"""Sample shareholder data for analysis"""
return {
"company_code": "005930",
"major_shareholders": [
{"name": "κ΅λ―Όμ°κΈκ³΅λ¨", "shares": 148_712_953, "percentage": 9.57, "type": "institutional"},
{"name": "μΌμ±λ¬Όμ°", "shares": 133_284_628, "percentage": 8.58, "type": "corporate"},
{"name": "μ΄μ¬μ©", "shares": 18_352_174, "percentage": 1.18, "type": "individual"},
{"name": "νλΌν¬", "shares": 15_248_921, "percentage": 0.98, "type": "individual"},
{"name": "μ΄λΆμ§", "shares": 12_419_832, "percentage": 0.80, "type": "individual"}
],
"total_shares": 5_969_782_550,
"treasury_shares": 64_726_919,
"dividend_history": [
{"year": 2023, "dividend_per_share": 1640, "total_dividend": 9787803622000},
{"year": 2022, "dividend_per_share": 1444, "total_dividend": 8617977438800},
{"year": 2021, "dividend_per_share": 1378, "total_dividend": 8225556394900}
]
}
@pytest.fixture
def sample_governance_data(self):
"""Sample governance data for testing"""
return {
"board_composition": {
"total_directors": 11,
"independent_directors": 5,
"inside_directors": 6,
"female_directors": 2,
"foreign_directors": 1
},
"committee_composition": {
"audit_committee": {"total": 3, "independent": 3},
"compensation_committee": {"total": 4, "independent": 3},
"governance_committee": {"total": 5, "independent": 2}
},
"board_meetings": {
"annual_meetings": 8,
"average_attendance": 92.5,
"key_decisions": 24
}
}
@pytest.mark.asyncio
async def test_analyze_major_shareholders(self, shareholder_analyzer, sample_shareholder_data):
"""Test major shareholders analysis"""
analysis = await shareholder_analyzer.analyze_major_shareholders(
shareholder_data=sample_shareholder_data,
min_percentage=0.5
)
assert analysis is not None
assert isinstance(analysis, dict)
# Should contain major shareholders information
assert "major_shareholders" in analysis
assert "top_5_concentration" in analysis
assert "ownership_summary" in analysis
# Should have correct number of major shareholders
major_shareholders = analysis["major_shareholders"]
assert len(major_shareholders) >= 3
# Should calculate concentrations correctly
top_5_concentration = analysis["top_5_concentration"]
assert isinstance(top_5_concentration, float)
assert 15 <= top_5_concentration <= 25 # Reasonable range
@pytest.mark.asyncio
async def test_calculate_ownership_structure(self, shareholder_analyzer, sample_shareholder_data):
"""Test ownership structure calculation"""
ownership = await shareholder_analyzer.calculate_ownership_structure(
shareholder_data=sample_shareholder_data,
market_data={"foreign_ownership": 52.37, "institutional_ownership": 31.28}
)
assert ownership is not None
assert isinstance(ownership, dict)
# Should contain key ownership metrics
assert "free_float" in ownership
assert "foreign_ownership" in ownership
assert "institutional_ownership" in ownership
assert "individual_ownership" in ownership
assert "insider_ownership" in ownership
# Ownership percentages should sum close to 100%
total = (ownership["foreign_ownership"] +
ownership["institutional_ownership"] +
ownership["individual_ownership"])
assert 95 <= total <= 105 # Allow for rounding
@pytest.mark.asyncio
async def test_analyze_dividend_history(self, shareholder_analyzer, sample_shareholder_data):
"""Test dividend history analysis"""
dividend_analysis = await shareholder_analyzer.analyze_dividend_history(
dividend_data=sample_shareholder_data["dividend_history"],
financial_metrics={"net_income": 65118000000000, "free_cash_flow": 45821000000000}
)
assert dividend_analysis is not None
assert isinstance(dividend_analysis, dict)
# Should contain dividend analysis metrics
assert "dividend_growth_rate" in dividend_analysis
assert "average_yield" in dividend_analysis
assert "dividend_consistency" in dividend_analysis
assert "payout_ratio" in dividend_analysis
assert "sustainability_score" in dividend_analysis
# Growth rate should be reasonable
growth_rate = dividend_analysis["dividend_growth_rate"]
assert isinstance(growth_rate, float)
assert -20 <= growth_rate <= 50 # Reasonable range
@pytest.mark.asyncio
async def test_calculate_governance_metrics(self, shareholder_analyzer, sample_governance_data):
"""Test governance metrics calculation"""
governance = await shareholder_analyzer.calculate_governance_metrics(
governance_data=sample_governance_data,
company_metrics={"company_age": 54, "market_cap": 435000000000000}
)
assert governance is not None
assert isinstance(governance, dict)
# Should contain governance metrics
assert "board_independence_ratio" in governance
assert "board_diversity_score" in governance
assert "committee_effectiveness" in governance
assert "overall_governance_score" in governance
assert "governance_grade" in governance
# Independence ratio should be calculated correctly
independence_ratio = governance["board_independence_ratio"]
expected_ratio = 5 / 11 * 100 # 5 independent out of 11 total
assert abs(independence_ratio - expected_ratio) < 1
# Overall score should be 0-100
overall_score = governance["overall_governance_score"]
assert 0 <= overall_score <= 100
# Grade should be letter grade
assert governance["governance_grade"] in ["A", "B", "C", "D", "F"]
@pytest.mark.asyncio
async def test_analyze_shareholder_concentration(self, shareholder_analyzer, sample_shareholder_data):
"""Test shareholder concentration analysis"""
concentration = await shareholder_analyzer.analyze_shareholder_concentration(
shareholder_data=sample_shareholder_data,
analysis_depth=10
)
assert concentration is not None
assert isinstance(concentration, dict)
# Should contain concentration metrics
assert "hhi_index" in concentration
assert "top_1_concentration" in concentration
assert "top_5_concentration" in concentration
assert "top_10_concentration" in concentration
assert "concentration_level" in concentration
# HHI should be calculated correctly
hhi = concentration["hhi_index"]
assert isinstance(hhi, float)
assert 0 <= hhi <= 10000 # HHI range
# Concentration level should be categorical
assert concentration["concentration_level"] in ["low", "moderate", "high", "very_high"]
@pytest.mark.asyncio
async def test_track_shareholder_changes(self, shareholder_analyzer):
"""Test shareholder change tracking"""
# Mock historical and current data
historical_data = [
{"name": "κ΅λ―Όμ°κΈκ³΅λ¨", "percentage": 9.2, "date": "2023-06-30"},
{"name": "μΌμ±λ¬Όμ°", "percentage": 8.8, "date": "2023-06-30"}
]
current_data = [
{"name": "κ΅λ―Όμ°κΈκ³΅λ¨", "percentage": 9.57, "date": "2023-12-31"},
{"name": "μΌμ±λ¬Όμ°", "percentage": 8.58, "date": "2023-12-31"}
]
changes = await shareholder_analyzer.track_shareholder_changes(
historical_data=historical_data,
current_data=current_data,
period="6M"
)
assert changes is not None
assert isinstance(changes, dict)
# Should contain change tracking information
assert "period" in changes
assert "significant_changes" in changes
assert "new_entrants" in changes
assert "exits" in changes
assert "net_changes" in changes
# Should identify changes correctly
significant_changes = changes["significant_changes"]
assert len(significant_changes) >= 2 # Both shareholders changed
@pytest.mark.asyncio
async def test_analyze_voting_power(self, shareholder_analyzer, sample_shareholder_data):
"""Test voting power analysis"""
voting_analysis = await shareholder_analyzer.analyze_voting_power(
shareholder_data=sample_shareholder_data,
voting_agreements=[{"parties": ["μ΄μ¬μ©", "νλΌν¬", "μ΄λΆμ§"], "type": "family_agreement"}]
)
assert voting_analysis is not None
assert isinstance(voting_analysis, dict)
# Should contain voting power metrics
assert "individual_voting_power" in voting_analysis
assert "coalition_analysis" in voting_analysis
assert "control_assessment" in voting_analysis
assert "shareholder_rights" in voting_analysis
# Should analyze family coalition
coalition_analysis = voting_analysis["coalition_analysis"]
assert len(coalition_analysis) > 0
@pytest.mark.asyncio
async def test_generate_shareholder_insights(self, shareholder_analyzer, sample_shareholder_data):
"""Test shareholder insights generation"""
insights = await shareholder_analyzer.generate_shareholder_insights(
shareholder_data=sample_shareholder_data,
market_context={"sector": "technology", "market_cap_tier": "large_cap"}
)
assert insights is not None
assert isinstance(insights, dict)
# Should contain key insights
assert "ownership_stability" in insights
assert "governance_strengths" in insights
assert "governance_concerns" in insights
assert "dividend_policy_assessment" in insights
assert "investment_implications" in insights
# Each insight category should have content
for category in insights.values():
assert isinstance(category, (str, list, dict))
@pytest.mark.asyncio
async def test_comprehensive_shareholder_analysis(self, shareholder_analyzer, sample_shareholder_data):
"""Test comprehensive shareholder analysis"""
comprehensive = await shareholder_analyzer.comprehensive_shareholder_analysis(
company_code="005930",
include_all_metrics=True
)
assert comprehensive is not None
assert isinstance(comprehensive, dict)
# Should contain all major analysis components
assert "ownership_analysis" in comprehensive
assert "governance_analysis" in comprehensive
assert "dividend_analysis" in comprehensive
assert "concentration_analysis" in comprehensive
assert "shareholder_summary" in comprehensive
# Should have summary insights
summary = comprehensive["shareholder_summary"]
assert "key_findings" in summary
assert "governance_score" in summary
assert "ownership_quality" in summary
@pytest.mark.asyncio
async def test_error_handling_insufficient_data(self, shareholder_analyzer):
"""Test error handling for insufficient data"""
# Test with insufficient shareholder data
insufficient_data = {"company_code": "005930", "major_shareholders": []}
with pytest.raises(InsufficientDataError):
await shareholder_analyzer.analyze_major_shareholders(insufficient_data)
@pytest.mark.asyncio
async def test_error_handling_invalid_parameters(self, shareholder_analyzer, sample_shareholder_data):
"""Test error handling for invalid parameters"""
# Test with invalid percentage threshold
with pytest.raises(MCPStockDetailsError):
await shareholder_analyzer.analyze_major_shareholders(
shareholder_data=sample_shareholder_data,
min_percentage=-1 # Invalid negative percentage
)
# Test with invalid analysis depth
with pytest.raises(MCPStockDetailsError):
await shareholder_analyzer.analyze_shareholder_concentration(
shareholder_data=sample_shareholder_data,
analysis_depth=0 # Invalid zero depth
)