From b9d21e2e2f7ae096c2f8a01bb142a685683b5b90 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:44:43 +0900 Subject: feat: add market sentiment filters (Fear & Greed, CryptoPanic, CryptoQuant) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- shared/tests/test_sentiment.py | 144 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 shared/tests/test_sentiment.py (limited to 'shared/tests/test_sentiment.py') 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() -- cgit v1.2.3