summaryrefslogtreecommitdiff
path: root/shared/tests
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 09:44:43 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 09:44:43 +0900
commitb9d21e2e2f7ae096c2f8a01bb142a685683b5b90 (patch)
treea031989228ded9ff1e6d47840124ea5dcc9a9a3c /shared/tests
parentbb2e387f870495703fd663ca8f525028c3a8ced5 (diff)
feat: add market sentiment filters (Fear & Greed, CryptoPanic, CryptoQuant)
- SentimentProvider: fetches Fear & Greed Index (free, no key), CryptoPanic news sentiment (free key), CryptoQuant exchange netflow (free key) - SentimentData: aggregated should_buy/should_block logic - Fear < 30 = buy opportunity, Greed > 80 = block buying - Negative news < -0.5 = block buying - Exchange outflow = bullish, inflow = bearish - Integrated into Asian Session RSI strategy as entry filter - All providers optional — disabled when API key missing - 14 sentiment tests + 386 total tests passing
Diffstat (limited to 'shared/tests')
-rw-r--r--shared/tests/test_sentiment.py144
1 files changed, 144 insertions, 0 deletions
diff --git a/shared/tests/test_sentiment.py b/shared/tests/test_sentiment.py
new file mode 100644
index 0000000..2caa266
--- /dev/null
+++ b/shared/tests/test_sentiment.py
@@ -0,0 +1,144 @@
+"""Tests for market sentiment module."""
+
+import pytest
+from unittest.mock import AsyncMock, MagicMock
+from shared.sentiment import SentimentData, SentimentProvider
+
+
+# --- SentimentData tests ---
+
+
+def test_sentiment_should_buy_on_fear():
+ s = SentimentData(fear_greed_value=15) # Extreme fear
+ assert s.should_buy is True
+
+
+def test_sentiment_should_not_buy_on_greed():
+ s = SentimentData(fear_greed_value=75) # Greed
+ assert s.should_buy is False
+
+
+def test_sentiment_should_block_extreme_greed():
+ s = SentimentData(fear_greed_value=85)
+ assert s.should_block is True
+
+
+def test_sentiment_should_block_very_negative_news():
+ s = SentimentData(news_sentiment=-0.6)
+ assert s.should_block is True
+
+
+def test_sentiment_no_block_on_neutral():
+ s = SentimentData(fear_greed_value=50, news_sentiment=0.0)
+ assert s.should_block is False
+
+
+def test_sentiment_should_buy_default_no_data():
+ s = SentimentData()
+ assert s.should_buy is True
+ assert s.should_block is False
+
+
+def test_sentiment_positive_news_allows_buy():
+ s = SentimentData(fear_greed_value=50, news_sentiment=0.3)
+ assert s.should_buy is True
+
+
+def test_sentiment_outflow_bullish():
+ s = SentimentData(exchange_netflow=-100.0) # Outflow = bullish
+ assert s.should_buy is True
+
+
+def test_sentiment_inflow_bearish():
+ s = SentimentData(fear_greed_value=50, exchange_netflow=100.0) # Inflow = bearish
+ assert s.should_buy is False
+
+
+# --- SentimentProvider tests ---
+
+
+@pytest.mark.asyncio
+async def test_provider_fetch_fear_greed():
+ provider = SentimentProvider()
+
+ mock_response = AsyncMock()
+ mock_response.status = 200
+ mock_response.json = AsyncMock(
+ return_value={"data": [{"value": "25", "value_classification": "Extreme Fear"}]}
+ )
+ mock_response.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_response.__aexit__ = AsyncMock(return_value=False)
+
+ mock_session = MagicMock()
+ mock_session.closed = False
+ mock_session.get = MagicMock(return_value=mock_response)
+ mock_session.close = AsyncMock()
+ provider._session = mock_session
+
+ value, label = await provider.fetch_fear_greed()
+ assert value == 25
+ assert label == "Extreme Fear"
+
+ await provider.close()
+
+
+@pytest.mark.asyncio
+async def test_provider_fetch_fear_greed_failure():
+ provider = SentimentProvider()
+
+ mock_response = AsyncMock()
+ mock_response.status = 500
+ mock_response.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_response.__aexit__ = AsyncMock(return_value=False)
+
+ mock_session = MagicMock()
+ mock_session.closed = False
+ mock_session.get = MagicMock(return_value=mock_response)
+ mock_session.close = AsyncMock()
+ provider._session = mock_session
+
+ value, label = await provider.fetch_fear_greed()
+ assert value is None
+
+ await provider.close()
+
+
+@pytest.mark.asyncio
+async def test_provider_news_disabled_without_key():
+ provider = SentimentProvider(cryptopanic_api_key="")
+ score, count = await provider.fetch_news_sentiment()
+ assert score is None
+ assert count == 0
+
+
+@pytest.mark.asyncio
+async def test_provider_netflow_disabled_without_key():
+ provider = SentimentProvider(cryptoquant_api_key="")
+ result = await provider.fetch_exchange_netflow()
+ assert result is None
+
+
+@pytest.mark.asyncio
+async def test_provider_get_sentiment_aggregates():
+ provider = SentimentProvider()
+
+ mock_response = AsyncMock()
+ mock_response.status = 200
+ mock_response.json = AsyncMock(
+ return_value={"data": [{"value": "20", "value_classification": "Extreme Fear"}]}
+ )
+ mock_response.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_response.__aexit__ = AsyncMock(return_value=False)
+
+ mock_session = MagicMock()
+ mock_session.closed = False
+ mock_session.get = MagicMock(return_value=mock_response)
+ mock_session.close = AsyncMock()
+ provider._session = mock_session
+
+ sentiment = await provider.get_sentiment("SOL")
+ assert sentiment.fear_greed_value == 20
+ assert sentiment.fear_greed_label == "Extreme Fear"
+ assert provider.cached is sentiment
+
+ await provider.close()