test_sentiment_tools.pyโข20.5 kB
"""์ฌ๋ฆฌ ์งํ ๋๊ตฌ ํ
์คํธ"""
import pytest
import random
import math
from datetime import datetime, timedelta
from unittest.mock import AsyncMock
from src.tools.sentiment_tools import MarketSentimentTool
from src.exceptions import DataValidationError, DatabaseConnectionError
class TestMarketSentimentTool:
"""์์ฅ ์ฌ๋ฆฌ ์งํ ๋๊ตฌ ํ
์คํธ"""
@pytest.fixture
def mock_db_manager(self):
"""Mock ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋งค๋์ """
return AsyncMock()
@pytest.fixture
def mock_cache_manager(self):
"""Mock ์บ์ ๋งค๋์ """
return AsyncMock()
@pytest.fixture
def sentiment_tool(self, mock_db_manager, mock_cache_manager):
"""์ฌ๋ฆฌ ์งํ ๋๊ตฌ ์ธ์คํด์ค"""
return MarketSentimentTool(mock_db_manager, mock_cache_manager)
@pytest.fixture
def sample_sentiment_data(self):
"""์ํ ์ฌ๋ฆฌ ์งํ ๋ฐ์ดํฐ"""
base_date = datetime.now().date()
return {
"put_call_ratio": [
{
"date": base_date - timedelta(days=i),
"put_volume": 85000000 + random.randint(-10000000, 10000000),
"call_volume": 120000000 + random.randint(-15000000, 15000000),
"put_call_ratio": 0.7 + random.uniform(-0.2, 0.2),
"put_oi": 950000 + random.randint(-100000, 100000),
"call_oi": 1200000 + random.randint(-150000, 150000)
}
for i in range(30)
],
"vkospi_data": [
{
"date": base_date - timedelta(days=i),
"vkospi_value": 22.5 + random.uniform(-5.0, 5.0),
"change": random.uniform(-3.0, 3.0),
"change_rate": random.uniform(-15.0, 15.0),
"high": 25.0 + random.uniform(-3.0, 3.0),
"low": 20.0 + random.uniform(-3.0, 3.0)
}
for i in range(30)
],
"market_indicators": [
{
"date": base_date - timedelta(days=i),
"kospi_close": 2650 + random.uniform(-100, 100),
"kospi_change_rate": random.uniform(-3.0, 3.0),
"volume_ratio": 0.8 + random.uniform(-0.3, 0.3),
"advancing_issues": random.randint(600, 900),
"declining_issues": random.randint(500, 800),
"new_highs": random.randint(5, 50),
"new_lows": random.randint(5, 45)
}
for i in range(30)
],
"news_sentiment": [
{
"date": base_date - timedelta(days=i),
"positive_count": random.randint(20, 80),
"negative_count": random.randint(15, 75),
"neutral_count": random.randint(50, 150),
"sentiment_score": random.uniform(-1.0, 1.0),
"buzz_intensity": random.uniform(0.1, 1.0)
}
for i in range(30)
]
}
def test_tool_initialization(self, sentiment_tool, mock_db_manager, mock_cache_manager):
"""๋๊ตฌ ์ด๊ธฐํ ํ
์คํธ"""
assert sentiment_tool.name == "get_market_sentiment"
assert sentiment_tool.description is not None
assert "์ฌ๋ฆฌ" in sentiment_tool.description or "๊ฐ์ฑ" in sentiment_tool.description
assert sentiment_tool.db_manager == mock_db_manager
assert sentiment_tool.cache_manager == mock_cache_manager
def test_tool_definition(self, sentiment_tool):
"""๋๊ตฌ ์ ์ ํ
์คํธ"""
definition = sentiment_tool.get_tool_definition()
assert definition.name == "get_market_sentiment"
assert definition.description is not None
assert definition.inputSchema is not None
# ์
๋ ฅ ์คํค๋ง ๊ฒ์ฆ
schema = definition.inputSchema
assert schema["type"] == "object"
assert "properties" in schema
properties = schema["properties"]
assert "market" in properties
assert "indicators" in properties
assert "period" in properties
assert "include_fear_greed_index" in properties
# indicators ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ
indicators_prop = properties["indicators"]
assert indicators_prop["type"] == "array"
assert "items" in indicators_prop
@pytest.mark.asyncio
async def test_execute_comprehensive_sentiment(self, sentiment_tool, sample_sentiment_data):
"""์ข
ํฉ ์ฌ๋ฆฌ ์งํ ๋ถ์ ํ
์คํธ"""
# ์บ์ ๋ฏธ์ค
sentiment_tool.cache_manager.get.return_value = None
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋ต ์ค์ (์์๋๋ก ์ฟผ๋ฆฌ๋ ๊ฒ์ผ๋ก ์์)
sentiment_tool.db_manager.fetch_all.side_effect = [
sample_sentiment_data["put_call_ratio"],
sample_sentiment_data["vkospi_data"],
sample_sentiment_data["market_indicators"],
sample_sentiment_data["news_sentiment"]
]
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio", "vkospi", "market_breadth", "news_sentiment"],
"period": "30d",
"include_fear_greed_index": True
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
assert len(result) == 1
content = result[0]
assert content.type == "text"
# JSON ํ์ฑํ์ฌ ๋ด์ฉ ํ์ธ
import json
data = json.loads(content.text)
assert "timestamp" in data
assert "market" in data
assert "period" in data
assert "sentiment_indicators" in data
assert "fear_greed_index" in data
assert "overall_sentiment" in data
# Put/Call Ratio ๊ฒ์ฆ
sentiment_indicators = data["sentiment_indicators"]
assert "put_call_ratio" in sentiment_indicators
pcr = sentiment_indicators["put_call_ratio"]
assert "current_ratio" in pcr
assert "interpretation" in pcr
assert "trend" in pcr
# VKOSPI ๊ฒ์ฆ
assert "vkospi" in sentiment_indicators
vkospi = sentiment_indicators["vkospi"]
assert "current_value" in vkospi
assert "volatility_regime" in vkospi
# Fear & Greed Index ๊ฒ์ฆ
fgi = data["fear_greed_index"]
assert "score" in fgi
assert "category" in fgi
assert 0 <= fgi["score"] <= 100
assert fgi["category"] in ["๊ทน๋ ๊ณตํฌ", "๊ณตํฌ", "์ค๋ฆฝ", "ํ์", "๊ทน๋ ํ์"]
@pytest.mark.asyncio
async def test_execute_put_call_ratio_only(self, sentiment_tool, sample_sentiment_data):
"""Put/Call Ratio๋ง ๋ถ์ ํ
์คํธ"""
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = sample_sentiment_data["put_call_ratio"]
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio"],
"period": "30d"
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
content = result[0]
import json
data = json.loads(content.text)
assert "sentiment_indicators" in data
sentiment_indicators = data["sentiment_indicators"]
assert "put_call_ratio" in sentiment_indicators
assert len(sentiment_indicators) == 1 # ํ๋์ ์งํ๋ง
@pytest.mark.asyncio
async def test_execute_vkospi_analysis(self, sentiment_tool, sample_sentiment_data):
"""VKOSPI ๋ถ์ ํ
์คํธ"""
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = sample_sentiment_data["vkospi_data"]
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["vkospi"],
"period": "30d"
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
content = result[0]
import json
data = json.loads(content.text)
vkospi = data["sentiment_indicators"]["vkospi"]
assert "current_value" in vkospi
assert "volatility_regime" in vkospi
assert "trend_analysis" in vkospi
# ๋ณ๋์ฑ ์ฒด์ ํ์ธ
regime = vkospi["volatility_regime"]
assert regime in ["๋ฎ์", "๋ณดํต", "๋์", "๋งค์ฐ ๋์"]
@pytest.mark.asyncio
async def test_market_breadth_sentiment(self, sentiment_tool, sample_sentiment_data):
"""์์ฅ ํญ ๊ธฐ๋ฐ ์ฌ๋ฆฌ ๋ถ์ ํ
์คํธ"""
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = sample_sentiment_data["market_indicators"]
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["market_breadth"],
"period": "30d"
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
content = result[0]
import json
data = json.loads(content.text)
breadth = data["sentiment_indicators"]["market_breadth"]
assert "advance_decline_sentiment" in breadth
assert "volume_sentiment" in breadth
assert "new_highs_lows_ratio" in breadth
@pytest.mark.asyncio
async def test_news_sentiment_analysis(self, sentiment_tool, sample_sentiment_data):
"""๋ด์ค ์ฌ๋ฆฌ ๋ถ์ ํ
์คํธ"""
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = sample_sentiment_data["news_sentiment"]
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["news_sentiment"],
"period": "7d",
"include_buzz_analysis": True
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
content = result[0]
import json
data = json.loads(content.text)
news = data["sentiment_indicators"]["news_sentiment"]
assert "sentiment_score" in news
assert "sentiment_trend" in news
assert "buzz_analysis" in news
# ๋ฒ์ฆ ๋ถ์ ํ์ธ
buzz = news["buzz_analysis"]
assert "intensity_level" in buzz
assert "trending_topics" in buzz
def test_put_call_ratio_interpretation(self, sentiment_tool):
"""Put/Call Ratio ํด์ ํ
์คํธ"""
# ๊ทน๋ ๊ณตํฌ (๋์ Put/Call Ratio)
interpretation = sentiment_tool._interpret_put_call_ratio(1.2)
assert "๊ณตํฌ" in interpretation
# ์ค๋ฆฝ
interpretation = sentiment_tool._interpret_put_call_ratio(0.8)
assert "์ค๋ฆฝ" in interpretation or "๋ณดํต" in interpretation
# ํ์ (๋ฎ์ Put/Call Ratio)
interpretation = sentiment_tool._interpret_put_call_ratio(0.4)
assert "ํ์" in interpretation
def test_vkospi_volatility_regime(self, sentiment_tool):
"""VKOSPI ๋ณ๋์ฑ ์ฒด์ ๋ถ๋ฅ ํ
์คํธ"""
# ๋ฎ์ ๋ณ๋์ฑ
regime = sentiment_tool._classify_volatility_regime(15.0)
assert regime == "๋ฎ์"
# ๋ณดํต ๋ณ๋์ฑ
regime = sentiment_tool._classify_volatility_regime(22.0)
assert regime == "๋ณดํต"
# ๋์ ๋ณ๋์ฑ
regime = sentiment_tool._classify_volatility_regime(30.0)
assert regime == "๋์"
# ๋งค์ฐ ๋์ ๋ณ๋์ฑ
regime = sentiment_tool._classify_volatility_regime(40.0)
assert regime == "๋งค์ฐ ๋์"
def test_fear_greed_index_calculation(self, sentiment_tool):
"""Fear & Greed Index ๊ณ์ฐ ํ
์คํธ"""
# ์ํ ์งํ๋ค
indicators = {
"put_call_ratio": {"score": 30, "weight": 0.25}, # ๊ณตํฌ์ชฝ
"vkospi": {"score": 25, "weight": 0.20}, # ๊ณตํฌ์ชฝ
"market_breadth": {"score": 70, "weight": 0.25}, # ํ์์ชฝ
"news_sentiment": {"score": 60, "weight": 0.15}, # ์ฝ๊ฐ ํ์
"volume": {"score": 55, "weight": 0.15} # ์ค๋ฆฝ
}
fgi_score = sentiment_tool._calculate_fear_greed_index(indicators)
assert 0 <= fgi_score <= 100
assert isinstance(fgi_score, (int, float))
def test_fear_greed_category(self, sentiment_tool):
"""Fear & Greed ์นดํ
๊ณ ๋ฆฌ ๋ถ๋ฅ ํ
์คํธ"""
# ๊ทน๋ ๊ณตํฌ
category = sentiment_tool._get_fear_greed_category(15)
assert category == "๊ทน๋ ๊ณตํฌ"
# ๊ณตํฌ
category = sentiment_tool._get_fear_greed_category(35)
assert category == "๊ณตํฌ"
# ์ค๋ฆฝ
category = sentiment_tool._get_fear_greed_category(50)
assert category == "์ค๋ฆฝ"
# ํ์
category = sentiment_tool._get_fear_greed_category(70)
assert category == "ํ์"
# ๊ทน๋ ํ์
category = sentiment_tool._get_fear_greed_category(90)
assert category == "๊ทน๋ ํ์"
def test_sentiment_trend_analysis(self, sentiment_tool):
"""์ฌ๋ฆฌ ํธ๋ ๋ ๋ถ์ ํ
์คํธ"""
# ์์น ํธ๋ ๋
values = [20, 25, 30, 35, 40]
trend = sentiment_tool._analyze_sentiment_trend(values)
assert trend["direction"] == "์์น"
# ํ๋ฝ ํธ๋ ๋
values = [60, 55, 50, 45, 40]
trend = sentiment_tool._analyze_sentiment_trend(values)
assert trend["direction"] == "ํ๋ฝ"
# ๋ณดํฉ
values = [50, 48, 52, 49, 51]
trend = sentiment_tool._analyze_sentiment_trend(values)
assert trend["direction"] == "๋ณดํฉ"
@pytest.mark.asyncio
async def test_social_media_sentiment(self, sentiment_tool):
"""์์
๋ฏธ๋์ด ์ฌ๋ฆฌ ๋ถ์ ํ
์คํธ"""
# ์์
๋ฏธ๋์ด ๋ฐ์ดํฐ
social_data = [
{
"date": datetime.now().date() - timedelta(days=i),
"platform": "twitter",
"positive_mentions": random.randint(100, 500),
"negative_mentions": random.randint(50, 300),
"neutral_mentions": random.randint(200, 800),
"sentiment_score": random.uniform(-1.0, 1.0),
"engagement_rate": random.uniform(0.01, 0.15),
"trending_keywords": ["KOSPI", "์ฃผ์", "ํฌ์", "์ฆ์"]
}
for i in range(7)
]
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = social_data
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["social_media"],
"period": "7d"
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
content = result[0]
import json
data = json.loads(content.text)
social = data["sentiment_indicators"]["social_media"]
assert "sentiment_score" in social
assert "engagement_trend" in social
assert "trending_topics" in social
@pytest.mark.asyncio
async def test_cache_functionality(self, sentiment_tool):
"""์บ์ ๊ธฐ๋ฅ ํ
์คํธ"""
# ์บ์ ํํธ ์๋๋ฆฌ์ค
cached_data = {
"timestamp": datetime.now().isoformat(),
"market": "KOSPI",
"sentiment_indicators": {
"put_call_ratio": {"current_ratio": 0.75, "interpretation": "์ค๋ฆฝ"}
},
"overall_sentiment": "์ค๋ฆฝ"
}
sentiment_tool.cache_manager.get.return_value = cached_data
# ์คํ
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio"],
"period": "30d"
})
# ์บ์์์ ๋ฐ์ดํฐ ๋ฐํ ํ์ธ
content = result[0]
import json
data = json.loads(content.text)
assert data == cached_data
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ํธ์ถ ์์ ํ์ธ
sentiment_tool.db_manager.fetch_all.assert_not_called()
@pytest.mark.asyncio
async def test_error_handling(self, sentiment_tool):
"""์๋ฌ ์ฒ๋ฆฌ ํ
์คํธ"""
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.side_effect = DatabaseConnectionError("DB ์ฐ๊ฒฐ ์คํจ")
with pytest.raises(DatabaseConnectionError):
await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio"],
"period": "30d"
})
@pytest.mark.asyncio
async def test_invalid_parameters(self, sentiment_tool):
"""์๋ชป๋ ํ๋ผ๋ฏธํฐ ํ
์คํธ"""
# ์๋ชป๋ ์์ฅ
with pytest.raises(ValueError, match="Invalid market"):
await sentiment_tool.execute({
"market": "INVALID",
"indicators": ["put_call_ratio"],
"period": "30d"
})
# ๋น ์งํ ๋ชฉ๋ก
with pytest.raises(ValueError, match="At least one indicator"):
await sentiment_tool.execute({
"market": "KOSPI",
"indicators": [],
"period": "30d"
})
# ์๋ชป๋ ๊ธฐ๊ฐ
with pytest.raises(ValueError, match="Invalid period"):
await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio"],
"period": "invalid"
})
@pytest.mark.asyncio
async def test_insufficient_data_handling(self, sentiment_tool):
"""๋ฐ์ดํฐ ๋ถ์กฑ ์ฒ๋ฆฌ ํ
์คํธ"""
# ๋ฐ์ดํฐ๊ฐ ๋ถ์กฑํ ๊ฒฝ์ฐ
insufficient_data = [
{
"date": datetime.now().date(),
"put_volume": 85000000,
"call_volume": 120000000,
"put_call_ratio": 0.7
}
]
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = insufficient_data
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio"],
"period": "30d"
})
content = result[0]
import json
data = json.loads(content.text)
assert "warning" in data or "insufficient data" in str(data).lower()
def test_sentiment_aggregation(self, sentiment_tool):
"""์ฌ๋ฆฌ ์งํ ์ข
ํฉ ํ
์คํธ"""
# ๋ค์ํ ์งํ๋ค
indicators = {
"put_call_ratio": 0.85, # ์ฝ๊ฐ ๊ณตํฌ
"vkospi": 28.5, # ๋์ ๋ณ๋์ฑ (๊ณตํฌ)
"advance_decline": 0.65, # ์ฝ๊ฐ ๋ถ์ ์
"news_sentiment": 0.2 # ์ฝ๊ฐ ๊ธ์ ์
}
overall = sentiment_tool._aggregate_sentiment_indicators(indicators)
assert "score" in overall
assert "sentiment" in overall
assert "confidence" in overall
assert 0 <= overall["score"] <= 100
assert overall["sentiment"] in ["๋งค์ฐ ๋ถ์ ์ ", "๋ถ์ ์ ", "์ค๋ฆฝ", "๊ธ์ ์ ", "๋งค์ฐ ๊ธ์ ์ "]
@pytest.mark.asyncio
async def test_historical_comparison(self, sentiment_tool, sample_sentiment_data):
"""๊ณผ๊ฑฐ ๋๋น ์ฌ๋ฆฌ ๋ณํ ๋ถ์ ํ
์คํธ"""
sentiment_tool.cache_manager.get.return_value = None
sentiment_tool.db_manager.fetch_all.return_value = sample_sentiment_data["put_call_ratio"]
# ์คํ (๊ณผ๊ฑฐ ๋น๊ต ํฌํจ)
result = await sentiment_tool.execute({
"market": "KOSPI",
"indicators": ["put_call_ratio"],
"period": "30d",
"include_historical_comparison": True
})
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
content = result[0]
import json
data = json.loads(content.text)
assert "historical_comparison" in data
historical = data["historical_comparison"]
assert "vs_1week_ago" in historical
assert "vs_1month_ago" in historical
assert "percentile_rank" in historical