Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/), Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.2 KiB
Python
77 lines
3.2 KiB
Python
"""
|
|
core/analysis/news.py — Keyword-basiertes News-Sentiment
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
# Bullische Phrasen für WTI (Angebot ↓ / Nachfrage ↑ / Risiko ↑)
|
|
NEWS_BULLISH_KW = {
|
|
"supply cut", "production cut", "opec cut", "opec+ cut", "output cut",
|
|
"sanction", "embargo", "ban on", "blockade", "shutdown", "outage",
|
|
"disruption", "halt", "force majeure", "pipeline attack",
|
|
"attack", "strike on", "missile", "drone strike", "tension escalat",
|
|
"iran tension", "houthi", "red sea", "strait of hormuz", "war",
|
|
"conflict escalat", "retaliat", "threat",
|
|
"demand growth", "demand surge", "demand rise", "demand strong",
|
|
"stockpile draw", "inventory draw", "stocks drop", "stocks fall",
|
|
"stockpiles fall", "crude draw", "cushing draw", "eia draw",
|
|
"pipeline shutdown", "refinery fire", "gulf of mexico storm",
|
|
"hurricane", "winter storm", "cold snap",
|
|
"oil surge", "oil rally", "oil jump", "oil soar", "oil spike",
|
|
"crude rise", "crude rally", "wti rise", "wti surge", "wti rally",
|
|
}
|
|
|
|
# Bärische Phrasen für WTI (Angebot ↑ / Nachfrage ↓ / Entspannung)
|
|
NEWS_BEARISH_KW = {
|
|
"supply glut", "oversupply", "production increase", "output rise",
|
|
"production boost", "opec boost", "opec+ unwind", "spr release",
|
|
"strategic reserve release", "saudi increase",
|
|
"us production record", "shale boom", "permian growth",
|
|
"demand drop", "demand fall", "demand weak", "demand slump",
|
|
"recession", "slowdown", "weak economy", "china slowdown",
|
|
"stockpile build", "inventory build", "stocks rise", "stocks build",
|
|
"crude build", "stockpiles rise", "cushing build", "eia build",
|
|
"ceasefire", "truce", "deal reached", "agreement", "diplomatic",
|
|
"talks resume", "easing tension", "sanction lift", "sanction relief",
|
|
"oil drop", "oil plunge", "oil slide", "oil fall", "oil decline",
|
|
"crude drop", "crude plunge", "wti fall", "wti slide", "oil crash",
|
|
}
|
|
|
|
|
|
def calc_news_sentiment(headlines: list, half_life_hours: float = 8.0) -> dict:
|
|
"""
|
|
Bewertet Headlines per Keyword-Matching mit altersgewichtetem Decay.
|
|
score: -1.0 (klar bärisch) … +1.0 (klar bullisch)
|
|
"""
|
|
import time as _time
|
|
if not headlines:
|
|
return {"score": 0.0, "n_bull": 0.0, "n_bear": 0.0, "samples": []}
|
|
|
|
now = _time.time()
|
|
bull_w = bear_w = 0.0
|
|
samples = []
|
|
for h in headlines:
|
|
text = (h.get("title_original") or h.get("title") or "").lower()
|
|
if not text:
|
|
continue
|
|
age_h = max(0.0, (now - h.get("ts", now)) / 3600.0)
|
|
weight = 0.5 ** (age_h / max(1.0, half_life_hours))
|
|
b = sum(1 for kw in NEWS_BULLISH_KW if kw in text)
|
|
s = sum(1 for kw in NEWS_BEARISH_KW if kw in text)
|
|
if b > s:
|
|
bull_w += weight * (1 + 0.3 * (b - 1))
|
|
samples.append(("bull", h.get("title", "")[:70]))
|
|
elif s > b:
|
|
bear_w += weight * (1 + 0.3 * (s - 1))
|
|
samples.append(("bear", h.get("title", "")[:70]))
|
|
|
|
total = bull_w + bear_w
|
|
score = 0.0 if total < 0.1 else (bull_w - bear_w) / total
|
|
return {
|
|
"score": max(-1.0, min(1.0, score)),
|
|
"n_bull": round(bull_w, 1),
|
|
"n_bear": round(bear_w, 1),
|
|
"samples": samples[:5],
|
|
}
|