Gesamtempfehlung: Selbst-Kalibrierung, voller Ausrichtungs-Split, Block-Gruende,

Elliott entmachtet (v=144)

Vier Verbesserungen, alle aus dem Gemessenen abgeleitet — kein neues Signal.
Ausgangspunkt: die Karte hat genau eine belegte Funktion, das Veto; als Prognose
ist sie ein Muenzwurf (48-51 % ueber 1.071 Episoden).

(1) SELBST-KALIBRIERUNG. Neue Tabelle rec_outcomes: eine Zeile je EPISODE
    (Richtungswechsel, nicht je Minute), Auswertung ~alle 5 min gegen candles_m1
    nach 30/60 min, Snapshot rec_track, Zeile #vd-selfcal. Gegen 50 % zu lesen.
    Warum der groesste Hebel: die Qualitaet war bis heute unsichtbar und musste
    auf Nachfrage rueckwirkend gemessen werden. Dasselbe Muster
    (pbreak_predictions) hat den Ausfall des P(break)-Modells LIVE aufgedeckt.
    Mit 4 Szenarien getestet (tests_rec_outcomes.py). Zwei Fehlschlaege dabei
    waren Testfehler, keine Code-Fehler — dokumentiert.

(2) alignment_stats auf die VOLLE Historie (1.216 statt 40 Trades) und
    zusaetzlich je Lot. Absolute Euro-Vergleiche sind bei wechselnder
    Positionsgroesse ungueltig (Lehre 04.08.). Damit steht die einzige
    beidhaelftig robuste Aussage der Karte auf ihrer vollen Stichprobe.

(3) Block-Gruende sichtbar (block_mix -> #vd-blocks): "heute X % ohne Block ·
    entry_room … · min_conf …". Bisher nur per DB-Abfrage zu beantworten.

(4) ELLIOTT: Verdict-Gewicht 1,0 -> 0. Gemessen ueber 15.544 Zeilen hatte es mit
    1.384 Episoden die groesste Stichprobe der Tabelle und ist darin flach
    (-0,075/+0,032, 50 % Treffer) — bei 25,9 % Einfluss auf die Nadel, weil es
    in nur 1 % der Zeilen schweigt. Chip bleibt, Stimme entfaellt.
    ⚠ Folge: die Nadel schlaegt staerker aus (real +0,33 -> +1,00).

⚠ Namenskollision beim Bau gefunden und behoben: die neue Klasse hiess zuerst
.vd-track — so heisst bereits die Schiene der Bias-Nadel; sie waere
ueberschrieben worden. Jetzt .vd-selfcal.

Live verifiziert: alignment je Lot, block_mix (93 von 1381 ohne Block),
rec_outcomes angelegt, Elliott weight=0 bei erhaltenem Chip, v=144 ausgeliefert,
eine Instanz auf Port 8000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-06 23:21:52 +02:00
co-authored by Claude Opus 5
parent 4f85d2ce75
commit 874ee16e00
9 changed files with 705 additions and 48 deletions
+114 -30
View File
@@ -145,6 +145,22 @@ SCHEMA = [
)
""",
"""
CREATE TABLE IF NOT EXISTS rec_outcomes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, -- lokale Epoch, Beginn der EPISODE
signal TEXT, -- 'LONG' | 'SHORT'
conf_pct INTEGER,
tf TEXT,
price REAL, -- Kurs bei Ausgabe der Empfehlung
atr REAL, -- ATR_M5 zur Normierung
ret30 REAL, -- (Kurs[+30min] - price) x Richtung / atr
ret60 REAL,
hit30 INTEGER, -- 1 = Kurs lief in Signalrichtung
hit60 INTEGER,
done INTEGER -- 1 = beide Horizonte ausgewertet
)
""",
"""
CREATE TABLE IF NOT EXISTS verdict_votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, -- lokale Epoch
@@ -420,6 +436,52 @@ class HistoryLogger:
conn.commit()
return cur.lastrowid
def log_rec_outcome(self, *, signal: str, conf_pct: int, tf: str,
price: float, atr: float) -> int:
"""Loggt EINE Empfehlungs-EPISODE zur spaeteren Selbst-Kalibrierung.
⚠ WARUM EPISODEN UND NICHT ZEILEN: die Empfehlung wird ~1x/Minute
geloggt; eine Marktlage erzeugt dutzende identische Zeilen. Wer die zaehlt,
haelt 20.000 Faelle fuer eine Stichprobe, obwohl es 1.000 sind
(`analyze_signal_live.py`). Hier wird genau EINE Zeile je Richtungswechsel
geschrieben.
Ausgewertet wird separat (`engine._evaluate_rec_outcomes`) gegen
`candles_m1` — dasselbe Muster wie `pbreak_predictions`, das den Ausfall
des P(break)-Modells live aufgedeckt hat. Rueckgabe = Zeilen-ID."""
with self._lock, self._connect() as conn:
cur = conn.execute("""
INSERT INTO rec_outcomes (ts, signal, conf_pct, tf, price, atr)
VALUES (?,?,?,?,?,?)
""", (int(time.time()), signal, int(conf_pct or 0), tf or "",
float(price or 0.0), float(atr or 0.0)))
conn.commit()
return cur.lastrowid
def rec_accuracy(self, last_n: int = 200) -> dict:
"""Trefferquote der letzten `last_n` ausgewerteten Empfehlungs-Episoden.
⚠ Die Trefferquote ist gegen 50 % zu lesen, nicht gegen 0 — und der
Ø-Return gegen die Drift des Zeitraums. Gemessen ueber 1.071 Episoden lag
die Richtungstreffer-Quote bei 4851 % (`analyze_signal_live.py`); diese
Zeile macht laufend sichtbar, ob sich daran etwas aendert."""
with self._connect() as conn:
rows = conn.execute("""
SELECT signal, ret30, ret60, hit30, hit60 FROM rec_outcomes
WHERE done = 1 ORDER BY ts DESC LIMIT ?
""", (int(last_n),)).fetchall()
if not rows:
return {"n": 0}
def q(feld, hit):
v = [r[feld] for r in rows if r[feld] is not None]
h = [r[hit] for r in rows if r[hit] is not None]
return ((round(100.0 * sum(h) / len(h), 1) if h else None),
(round(sum(v) / len(v), 3) if v else None), len(h))
wr30, avg30, n30 = q("ret30", "hit30")
wr60, avg60, n60 = q("ret60", "hit60")
return {"n": len(rows), "wr30": wr30, "avg30": avg30, "n30": n30,
"wr60": wr60, "avg60": avg60, "n60": n60}
def pbreak_accuracy(self, period: str = "all") -> dict:
"""Trefferquote der Abprall/Durchbruch-Prognose (nur ausgewertete Zeilen)."""
since, until = self._range_to_ts(period)
@@ -521,38 +583,60 @@ class HistoryLogger:
"pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)}
return out
def alignment_stats(self, last_n: int = 40) -> dict:
"""Ausrichtungs-Split der letzten N geschlossenen Trades: liefen sie MIT,
GEGEN oder OHNE die Empfehlung (rec_signal beim Entry)? Für die ehrliche
Verhaltens-Anzeige im Order-Bestätigungsdialog (B0 als Software — die
gemessene Kern-Leckage sind diskretionäre Gegen-Signal-Trades)."""
def alignment_stats(self, last_n: int = 0) -> dict:
"""Ausrichtungs-Split: liefen die Trades MIT, GEGEN oder OHNE die Empfehlung?
⚠⚠ ZWEI AENDERUNGEN AM 2026-08-06, beide gemessen begruendet:
(1) **Volle Historie statt der letzten 40 Trades** (`last_n=0`). Ueber
1.216 geschlossene Trades ist der Befund beidhaelftig robust; ueber 40
war er eine Momentaufnahme. Das ist die einzige Richtungsaussage ueber
die Gesamtempfehlung, die den 2-Stichproben-Test besteht — sie sollte
auf der ganzen Stichprobe stehen.
(2) **Zusaetzlich JE LOT normiert** (`pnl_lot`). Absolute Euro-Vergleiche
ueber Zeitraeume mit unterschiedlicher Positionsgroesse sind ungueltig
— am 04.08. sah ein reiner Groessenzuwachs wie ein Strategie-Effekt aus
(Ø +55 € gegen +19 €, je Lot aber identisch). Das Order-Dialog-Feld
zeigt deshalb beides.
Gemessener Stand (06.08., ganze Historie, je Lot):
MIT n=354 WR 56,5 % +3,22 €/Lot (H1 +0,37 / H2 +5,62)
GEGEN n= 87 WR 40,2 % 6,60 €/Lot (H1 7,01 / H2 5,68)
OHNE n=775 WR 56,6 % +0,36 €/Lot (kippt zwischen den Haelften)
MIT und OHNE sind praktisch gleich — der Empfehlung zu FOLGEN bringt
gemessen nichts, nur sie zu VERLETZEN kostet. Sie ist ein Veto, keine
Prognose.
"""
sql = ("SELECT direction, rec_signal, pnl, lots FROM trades "
"WHERE exit_time IS NOT NULL AND pnl IS NOT NULL AND lots > 0 "
"ORDER BY exit_time DESC")
args: tuple = ()
if last_n and last_n > 0:
sql += " LIMIT ?"
args = (last_n,)
with self._connect() as conn:
rows = conn.execute("""
SELECT direction, rec_signal, pnl FROM trades
WHERE exit_time IS NOT NULL AND pnl IS NOT NULL
ORDER BY exit_time DESC LIMIT ?
""", (last_n,)).fetchall()
out = {}
for key, match in (("with", True), ("against", False)):
grp = []
for r in rows:
rec = (r["rec_signal"] or "").upper()
if rec not in ("LONG", "SHORT"):
continue
aligned = (rec == "LONG") == (r["direction"] == "BUY")
if aligned == match:
grp.append(r["pnl"])
rows = conn.execute(sql, args).fetchall()
def fasse(grp: list) -> dict:
n = len(grp)
out[key] = {"n": n,
"wr": round(100 * sum(1 for p in grp if p > 0) / n) if n else None,
"pnl": round(sum(grp), 2)}
none_grp = [r["pnl"] for r in rows
if (r["rec_signal"] or "").upper() not in ("LONG", "SHORT")]
n = len(none_grp)
out["none"] = {"n": n,
"wr": round(100 * sum(1 for p in none_grp if p > 0) / n) if n else None,
"pnl": round(sum(none_grp), 2)}
return out
if not n:
return {"n": 0, "wr": None, "pnl": 0.0, "pnl_lot": None}
pnl = sum(r["pnl"] for r in grp)
lots = sum(r["lots"] for r in grp) or 0.0
return {"n": n,
"wr": round(100 * sum(1 for r in grp if r["pnl"] > 0) / n),
"pnl": round(pnl, 2),
"pnl_lot": round(pnl / lots, 2) if lots > 0 else None}
mit, gegen, ohne = [], [], []
for r in rows:
rec = (r["rec_signal"] or "").upper()
if rec not in ("LONG", "SHORT"):
ohne.append(r)
elif (rec == "LONG") == (r["direction"] == "BUY"):
mit.append(r)
else:
gegen.append(r)
return {"with": fasse(mit), "against": fasse(gegen), "none": fasse(ohne)}
# ══════════════════════════════════════════
# STATISTIK-QUERIES