Add new feature to handle user input
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
sentiment.py — Nieuws sentiment engine
|
||||
Bron: CryptoPanic API (gratis tier beschikbaar)
|
||||
Scoring: gewogen op votes + recency, -1.0 tot +1.0
|
||||
Geen zware ML modellen — rule-based op CryptoPanic's eigen
|
||||
bullish/bearish labels + vote ratio. Snel en betrouwbaar.
|
||||
"""
|
||||
import os
|
||||
import httpx
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CRYPTOPANIC_BASE = "https://cryptopanic.com/api/v1"
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewsItem:
|
||||
title: str
|
||||
published: datetime
|
||||
source: str
|
||||
url: str
|
||||
currencies: list[str]
|
||||
sentiment: str # "positive" | "negative" | "neutral"
|
||||
votes_pos: int
|
||||
votes_neg: int
|
||||
score: float # berekend gewogen score
|
||||
|
||||
|
||||
@dataclass
|
||||
class SentimentResult:
|
||||
symbol: str # bijv. "BTC"
|
||||
score: float # -1.0 tot +1.0
|
||||
label: str # "positief" | "negatief" | "neutraal"
|
||||
confidence: float # 0.0 tot 1.0
|
||||
news_count: int
|
||||
recent_news: list[NewsItem] = field(default_factory=list)
|
||||
last_updated: datetime = field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
def _calculate_item_score(item: dict) -> float:
|
||||
"""
|
||||
Bereken score per nieuwsitem op basis van:
|
||||
- CryptoPanic kind label (bullish/bearish)
|
||||
- Vote verhouding
|
||||
- Recency (items ouder dan 12u wegen minder)
|
||||
"""
|
||||
votes = item.get("votes", {})
|
||||
pos = votes.get("positive", 0)
|
||||
neg = votes.get("negative", 0)
|
||||
total = pos + neg
|
||||
|
||||
# Vote-gebaseerde score
|
||||
if total > 0:
|
||||
vote_score = (pos - neg) / total # -1 tot +1
|
||||
else:
|
||||
vote_score = 0.0
|
||||
|
||||
# CryptoPanic kind label
|
||||
kind = item.get("kind", "news")
|
||||
if kind == "bullish":
|
||||
label_score = 0.6
|
||||
elif kind == "bearish":
|
||||
label_score = -0.6
|
||||
else:
|
||||
label_score = 0.0
|
||||
|
||||
# Recency weight
|
||||
try:
|
||||
published = datetime.fromisoformat(
|
||||
item["published_at"].replace("Z", "+00:00")
|
||||
).replace(tzinfo=None)
|
||||
age_hours = (datetime.utcnow() - published).total_seconds() / 3600
|
||||
recency = max(0.2, 1.0 - (age_hours / 24)) # 24u = 0.2 gewicht
|
||||
except Exception:
|
||||
recency = 0.5
|
||||
|
||||
# Gewogen combinatie
|
||||
score = (vote_score * 0.4 + label_score * 0.6) * recency
|
||||
return round(score, 3)
|
||||
|
||||
|
||||
async def fetch_sentiment(symbol: str) -> SentimentResult:
|
||||
"""
|
||||
Haal sentiment op voor een specifiek crypto symbol.
|
||||
symbol = coin code zonder /EUR, bijv. "BTC", "ETH"
|
||||
"""
|
||||
api_key = os.getenv("CRYPTOPANIC_API_KEY", "")
|
||||
|
||||
if not api_key:
|
||||
logger.warning("CRYPTOPANIC_API_KEY niet ingesteld — dummy sentiment")
|
||||
return _dummy_sentiment(symbol)
|
||||
|
||||
url = f"{CRYPTOPANIC_BASE}/posts/"
|
||||
params = {
|
||||
"auth_token": api_key,
|
||||
"currencies": symbol,
|
||||
"filter": "hot",
|
||||
"public": "true",
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.get(url, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
results = data.get("results", [])
|
||||
news_items = []
|
||||
scores = []
|
||||
|
||||
for item in results[:20]: # Maximaal 20 recente items
|
||||
item_score = _calculate_item_score(item)
|
||||
scores.append(item_score)
|
||||
|
||||
currencies = [
|
||||
c.get("code", "") for c in item.get("currencies", [])
|
||||
]
|
||||
|
||||
votes = item.get("votes", {})
|
||||
news_items.append(NewsItem(
|
||||
title=item.get("title", ""),
|
||||
published=datetime.fromisoformat(
|
||||
item["published_at"].replace("Z", "+00:00")
|
||||
).replace(tzinfo=None),
|
||||
source=item.get("source", {}).get("title", ""),
|
||||
url=item.get("url", ""),
|
||||
currencies=currencies,
|
||||
sentiment="positive" if item_score > 0.1 else (
|
||||
"negative" if item_score < -0.1 else "neutral"
|
||||
),
|
||||
votes_pos=votes.get("positive", 0),
|
||||
votes_neg=votes.get("negative", 0),
|
||||
score=item_score,
|
||||
))
|
||||
|
||||
if not scores:
|
||||
return _dummy_sentiment(symbol)
|
||||
|
||||
avg_score = sum(scores) / len(scores)
|
||||
confidence = min(1.0, len(scores) / 10) # 10+ items = max confidence
|
||||
|
||||
if avg_score > 0.15:
|
||||
label = "positief"
|
||||
elif avg_score < -0.15:
|
||||
label = "negatief"
|
||||
else:
|
||||
label = "neutraal"
|
||||
|
||||
return SentimentResult(
|
||||
symbol=symbol,
|
||||
score=round(avg_score, 3),
|
||||
label=label,
|
||||
confidence=round(confidence, 2),
|
||||
news_count=len(scores),
|
||||
recent_news=news_items[:5], # Top 5 voor dashboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Sentiment fetch mislukt voor {symbol}: {e}")
|
||||
return _dummy_sentiment(symbol)
|
||||
|
||||
|
||||
def _dummy_sentiment(symbol: str) -> SentimentResult:
|
||||
"""Fallback als API niet beschikbaar is."""
|
||||
return SentimentResult(
|
||||
symbol=symbol,
|
||||
score=0.0,
|
||||
label="neutraal",
|
||||
confidence=0.0,
|
||||
news_count=0,
|
||||
recent_news=[],
|
||||
)
|
||||
|
||||
|
||||
async def fetch_all_sentiments(symbols: list[str]) -> dict[str, SentimentResult]:
|
||||
"""Haal sentiment op voor meerdere coins parallel."""
|
||||
tasks = [fetch_sentiment(s) for s in symbols]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
output = {}
|
||||
for symbol, result in zip(symbols, results):
|
||||
if isinstance(result, SentimentResult):
|
||||
output[symbol] = result
|
||||
else:
|
||||
output[symbol] = _dummy_sentiment(symbol)
|
||||
|
||||
return output
|
||||
Reference in New Issue
Block a user