Files
AH-Oil-Trader/core/analysis/news.py
T
Axel HocksandClaude Opus 5 94fbde1dba News-Sentiment war gesaettigt — Beweismengen-Gewichtung eingebaut
Anlass: Frage nach X-Anbindung fuer den Copiloten. Vorher geprueft, ob der
vorhandene Textstrom ueberhaupt traegt. Er trug nichts, weil der Score kaputt war.

calc_news_sentiment bildete ein REINES VERHAELTNIS (bull-bear)/total, ohne Bezug
zur Beweismenge. Eine einzige bullische Schlagzeile bei null baerischen ergab
+1,00. Diagnose:
- 173 von 173 Log-Eintraegen auf +/-1,00 gesaettigt (100 %)
- in 100 % der Faelle eine Seite exakt 0
- Median-Beweismenge nur 2,8 gewichtete Headlines
- ueber die DB (54.548 Zeilen): 63,9 % auf exakt +1,00
Real am 03.08.: +1,00 waehrend WTI -5,9 % stand. Der Wert trug ein Bit und
loeste im Entry-Check dauerhaft falsche "News-Konflikt"-Warnungen aus.
Die Keyword-Listen sind NICHT die Ursache (56/48, keine Ueberschneidung).

Fix: Shrinkage score = raw * total/(total+4,0). Die 4,0 ist begruendet, nicht
optimiert: bei der realen Median-Beweismenge 2,8 bleibt eine einseitige
3-Headline-Lage unter der 0,5-Warnschwelle, ~10 Headlines ergeben 0,71.
Zusaetzlich raw_score und evidence im Rueckgabewert, damit unterscheidbar wird
ob "ausgewogene Lage" oder "kaum Daten". Leer-Fall liefert dieselben Felder.
6 Szenarien getestet, live verifiziert: bei identischer Beweislage
(bull=1.8, bear=0.0) +1,00 -> +0,31.

EHRLICH: das repariert die Kalibrierung, nicht die Aussagekraft. Parallel
gemessen (Forward-Return 120 min, 38.213 Paare, 84 Tage, 2 Haelften, gegen die
Regime-Drift gelesen): der stark-bullische Bucket liegt in BEIDEN Haelften bei
null bis negativ, der baerische kippt das Vorzeichen. News-Sentiment ist nicht
robust praediktiv. Bleibt Kontext-Chip ohne Verdict-Stimme.

Methodik-Falle dokumentiert: der erste Messlauf nutzte candles_m1 (nur ~14 Tage)
-> bisect lieferte fuer aeltere Zeitstempel denselben Randwert, alle
Forward-Returns exakt 0,0000. Erst der Lauf mit MT5-M5-Bars plus
Deckungspruefung war gueltig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:25:47 +02:00

114 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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",
}
# Wie viele gewichtete Headlines es braucht, damit der Score seinen Rohwert
# annähernd erreicht (Shrinkage-Konstante, s. Begründung in `calc_news_sentiment`).
# 4,0 gewählt, weil die reale Median-Beweismenge bei 2,8 liegt: damit bleibt eine
# typische, einseitige 3-Headline-Lage UNTER der 0,5-Warnschwelle des Entry-Checks.
_EVIDENCE_K = 4.0
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:
# Felder identisch zum Normalfall halten — sonst wirft ein Konsument, der
# `raw_score`/`evidence` liest, ausgerechnet im „keine News"-Fall.
return {"score": 0.0, "n_bull": 0.0, "n_bear": 0.0,
"raw_score": 0.0, "evidence": 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
raw = 0.0 if total < 0.1 else (bull_w - bear_w) / total
# ── BEWEISMENGEN-GEWICHTUNG (Fix 2026-08-03) ─────────────────────────────
# ⚠ Vorher war der Score ein REINES VERHÄLTNIS: `(bull-bear)/total`. Eine
# einzige bullische Schlagzeile bei null bearischen ergab damit +1,00 —
# maximale Überzeugung aus EINEM Treffer. Gemessen an 173 Log-Einträgen:
# **100 % standen auf ±1,00**, und in **100 %** der Fälle war eine Seite
# exakt 0; die Median-Beweismenge betrug **2,8** gewichtete Headlines.
# Über die gesamte DB (54.548 Zeilen) klebten **63,9 % auf exakt +1,00**.
# Real am 03.08.: Sentiment +1,00 („bull=1.8, bear=0.0"), während WTI
# 5,9 % stand. Der Wert trug damit faktisch EIN Bit (das Vorzeichen) und
# löste im Entry-Check dauerhaft falsche „News-Konflikt"-Warnungen aus.
# Korrektur: zur Null schrumpfen, solange die Beweislage dünn ist
# (klassische Shrinkage). Bei der Median-Beweismenge 2,8 wird aus 1,00
# noch 0,41 — unter der 0,5-Warnschwelle des Entry-Checks; erst ~10
# gewichtete Headlines ergeben 0,71.
# ⚠ EHRLICH: Das repariert die KALIBRIERUNG, nicht die Aussagekraft. Der
# Score bleibt das Vorzeichen weniger Keyword-Treffer, und dessen
# Prädiktivität ist gemessen NICHT robust (Forward-Return über 38k Paare,
# 2 Hälften: der bearische Bucket kippt das Vorzeichen, der stark-bullische
# liegt in BEIDEN Hälften bei null bis negativ). `k` ist eine begründete
# Setzung, kein optimierter Wert — es gibt hier nichts zu optimieren,
# solange das Signal selbst nichts vorhersagt.
conf = total / (total + _EVIDENCE_K) if total > 0 else 0.0
score = raw * conf
return {
"score": max(-1.0, min(1.0, score)),
"n_bull": round(bull_w, 1),
"n_bear": round(bear_w, 1),
# Rohwert + Beweismenge mitliefern: so ist im UI/Copilot erkennbar, ob
# ein schwacher Score „ausgewogene Nachrichtenlage" oder „kaum Daten" heißt.
"raw_score": round(max(-1.0, min(1.0, raw)), 3),
"evidence": round(total, 2),
"samples": samples[:5],
}