summaryrefslogtreecommitdiff
path: root/shared/src
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:26:52 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:26:52 +0900
commit53cadcf7e34f05f77082e84f0696b56bcbcbae36 (patch)
treee02650e10c4d5727bc1e32e27788c17327fa46f7 /shared/src
parentf5521da2876a2c19afc24f370b3258f2be95f81a (diff)
refactor: remove all crypto/Binance code, update to US stock symbols
Diffstat (limited to 'shared/src')
-rw-r--r--shared/src/shared/sentiment.py204
1 files changed, 10 insertions, 194 deletions
diff --git a/shared/src/shared/sentiment.py b/shared/src/shared/sentiment.py
index bc62efe..8213b47 100644
--- a/shared/src/shared/sentiment.py
+++ b/shared/src/shared/sentiment.py
@@ -1,19 +1,9 @@
-"""Market sentiment data from free APIs.
-
-Supports:
-- Fear & Greed Index (alternative.me) — no API key needed
-- CryptoPanic news sentiment — free API key from cryptopanic.com
-- CryptoQuant exchange netflow — free API key from cryptoquant.com
-
-All providers are optional. If API key is missing, the provider is disabled.
-"""
+"""Market sentiment data."""
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
-import aiohttp
-
logger = logging.getLogger(__name__)
@@ -21,199 +11,25 @@ logger = logging.getLogger(__name__)
class SentimentData:
"""Aggregated sentiment snapshot."""
- fear_greed_value: int | None = None # 0-100
- fear_greed_label: str | None = (
- None # "Extreme Fear", "Fear", "Neutral", "Greed", "Extreme Greed"
- )
- news_sentiment: float | None = None # -1.0 (bearish) to 1.0 (bullish)
+ fear_greed_value: int | None = None
+ fear_greed_label: str | None = None
+ news_sentiment: float | None = None
news_count: int = 0
- exchange_netflow: float | None = (
- None # Positive = inflow (bearish), Negative = outflow (bullish)
- )
+ exchange_netflow: float | None = None
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@property
def should_buy(self) -> bool:
- """Simple aggregated buy signal from sentiment."""
- score = 0
- checks = 0
-
- if self.fear_greed_value is not None:
- checks += 1
- if self.fear_greed_value < 30:
- score += 1 # Fear = buy opportunity
- elif self.fear_greed_value > 70:
- score -= 1 # Greed = avoid buying
-
- if self.news_sentiment is not None:
- checks += 1
- if self.news_sentiment > 0.1:
- score += 1 # Positive news
- elif self.news_sentiment < -0.3:
- score -= 1 # Very negative news = avoid
-
- if self.exchange_netflow is not None:
- checks += 1
- if self.exchange_netflow < 0:
- score += 1 # Outflow = bullish (coins leaving exchanges)
- elif self.exchange_netflow > 0:
- score -= 1 # Inflow = bearish (coins entering exchanges to sell)
-
- if checks == 0:
- return True # No data, allow by default
-
- return score >= 0 # Net neutral or positive = allow
+ if self.fear_greed_value is not None and self.fear_greed_value > 70:
+ return False
+ if self.news_sentiment is not None and self.news_sentiment < -0.3:
+ return False
+ return True
@property
def should_block(self) -> bool:
- """Strong bearish signal — block all buying."""
- # Block on extreme greed
if self.fear_greed_value is not None and self.fear_greed_value > 80:
return True
- # Block on very negative news
if self.news_sentiment is not None and self.news_sentiment < -0.5:
return True
return False
-
-
-class SentimentProvider:
- """Fetches sentiment data from multiple free APIs."""
-
- def __init__(
- self,
- cryptopanic_api_key: str = "",
- cryptoquant_api_key: str = "",
- ) -> None:
- self._cryptopanic_key = cryptopanic_api_key
- self._cryptoquant_key = cryptoquant_api_key
- self._session: aiohttp.ClientSession | None = None
- self._cached: SentimentData | None = None
- self._cache_ttl: int = 300 # 5 minutes cache
-
- async def _ensure_session(self) -> aiohttp.ClientSession:
- if self._session is None or self._session.closed:
- self._session = aiohttp.ClientSession()
- return self._session
-
- async def fetch_fear_greed(self) -> tuple[int | None, str | None]:
- """Fetch Fear & Greed Index from alternative.me (no key needed)."""
- try:
- session = await self._ensure_session()
- url = "https://api.alternative.me/fng/?limit=1"
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
- if resp.status != 200:
- logger.warning("Fear & Greed API returned %d", resp.status)
- return None, None
- data = await resp.json()
- entry = data.get("data", [{}])[0]
- value = int(entry.get("value", 0))
- label = entry.get("value_classification", "")
- return value, label
- except Exception as exc:
- logger.warning("Fear & Greed fetch failed: %s", exc)
- return None, None
-
- async def fetch_news_sentiment(self, currency: str = "SOL") -> tuple[float | None, int]:
- """Fetch news sentiment from CryptoPanic.
-
- Returns (sentiment_score, news_count).
- Sentiment score: -1.0 (all bearish) to 1.0 (all bullish).
- """
- if not self._cryptopanic_key:
- return None, 0
-
- try:
- session = await self._ensure_session()
- url = (
- f"https://cryptopanic.com/api/v1/posts/"
- f"?auth_token={self._cryptopanic_key}"
- f"&currencies={currency}"
- f"&kind=news"
- f"&filter=hot"
- f"&limit=10"
- )
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
- if resp.status != 200:
- logger.warning("CryptoPanic API returned %d", resp.status)
- return None, 0
- data = await resp.json()
- results = data.get("results", [])
- if not results:
- return None, 0
-
- # CryptoPanic provides votes: positive, negative, important, etc.
- total_positive = 0
- total_negative = 0
- count = 0
- for post in results:
- votes = post.get("votes", {})
- pos = votes.get("positive", 0)
- neg = votes.get("negative", 0)
- total_positive += pos
- total_negative += neg
- count += 1
-
- total_votes = total_positive + total_negative
- if total_votes == 0:
- return 0.0, count
-
- sentiment = (total_positive - total_negative) / total_votes
- return sentiment, count
- except Exception as exc:
- logger.warning("CryptoPanic fetch failed: %s", exc)
- return None, 0
-
- async def fetch_exchange_netflow(self, symbol: str = "sol") -> float | None:
- """Fetch exchange netflow from CryptoQuant.
-
- Returns netflow value. Positive = inflow (bearish), Negative = outflow (bullish).
- """
- if not self._cryptoquant_key:
- return None
-
- try:
- session = await self._ensure_session()
- url = (
- f"https://api.cryptoquant.com/v1/{symbol}/exchange-flows/netflow?window=day&limit=1"
- )
- headers = {"Authorization": f"Bearer {self._cryptoquant_key}"}
- async with session.get(
- url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)
- ) as resp:
- if resp.status != 200:
- logger.warning("CryptoQuant API returned %d", resp.status)
- return None
- data = await resp.json()
- result = data.get("result", {}).get("data", [])
- if result:
- return float(result[0].get("netflow", 0))
- return None
- except Exception as exc:
- logger.warning("CryptoQuant fetch failed: %s", exc)
- return None
-
- async def get_sentiment(self, currency: str = "SOL") -> SentimentData:
- """Fetch all sentiment data and return aggregated result."""
- fg_value, fg_label = await self.fetch_fear_greed()
- news_score, news_count = await self.fetch_news_sentiment(currency)
- netflow = await self.fetch_exchange_netflow(currency.lower())
-
- sentiment = SentimentData(
- fear_greed_value=fg_value,
- fear_greed_label=fg_label,
- news_sentiment=news_score,
- news_count=news_count,
- exchange_netflow=netflow,
- )
-
- self._cached = sentiment
- return sentiment
-
- @property
- def cached(self) -> SentimentData | None:
- """Return last fetched sentiment data."""
- return self._cached
-
- async def close(self) -> None:
- if self._session and not self._session.closed:
- await self._session.close()