"""Selbst-Kalibrierung: Episoden-Logging und Auswertung der Empfehlungen. HINTERGRUND (2026-08-06): die Qualität der Gesamtempfehlung war unsichtbar und musste auf Nachfrage rückwirkend gemessen werden (48–51 % Treffer über 1.071 Episoden). `rec_outcomes` schließt die Lücke — dasselbe Muster wie `pbreak_predictions`, das den Ausfall des P(break)-Modells LIVE aufgedeckt hat. ⚠ Die zwei Fehlschläge beim ersten Schreiben dieser Tests waren TESTFEHLER, keine Code-Fehler: die Test-Kerzen des Vorabschnitts lagen im Fenster des nächsten, und die damals kopierte Live-DB enthielt ECHTE `candles_m1` im vermeintlich leeren Fenster. Beides ist hier konstruktiv ausgeschlossen — jeder Test bekommt eine frische, leere DB (s. `conftest.py`). """ import sqlite3 import time OFF = 3 * 3600 # candles_m1.time ist ROHE Broker-Zeit (UTC+3) def _episode(engine, sig): """Der Episoden-Teil aus `_run_analysis`, isoliert nachgestellt.""" if sig in ("LONG", "SHORT") and sig != engine._rec_epi_sig: engine.history.log_rec_outcome(signal=sig, conf_pct=70, tf="M5", price=75.0, atr=0.2) if sig in ("LONG", "SHORT", "WARTEN"): engine._rec_epi_sig = sig if sig != "WARTEN" else None def _zeilen(db): with sqlite3.connect(db) as c: return c.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0] # ── Episoden-Bildung ───────────────────────────────────────────────────── def test_gleiche_richtung_erzeugt_eine_zeile(engine, db): """⚠ Der Kern: die Empfehlung wird ~1x/Minute geloggt. Wer die Zeilen zählt, hält 20.000 Fälle für eine Stichprobe, obwohl es 1.000 sind.""" for _ in range(5): _episode(engine, "LONG") assert _zeilen(db) == 1 def test_flip_ist_eine_neue_episode(engine, db): for _ in range(3): _episode(engine, "LONG") for _ in range(3): _episode(engine, "SHORT") assert _zeilen(db) == 2 def test_warten_re_armt(engine, db): _episode(engine, "SHORT") _episode(engine, "WARTEN") _episode(engine, "WARTEN") assert _zeilen(db) == 1 # WARTEN selbst erzeugt nichts _episode(engine, "SHORT") assert _zeilen(db) == 2 # danach ist SHORT wieder neu # ── Auswertung gegen candles_m1 ────────────────────────────────────────── def _setze(db, *, ts, signal, price, atr, kerzen): with sqlite3.connect(db) as c: c.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) " "VALUES (?,?,?,?,?,?)", (ts, signal, 70, "M5", price, atr)) for minuten, kurs in kerzen: c.execute("INSERT OR REPLACE INTO candles_m1 " "(time,o,h,l,c,spread,tick_volume,symbol) " "VALUES (?,?,?,?,?,?,?,?)", (ts + minuten * 60 + OFF, kurs, kurs, kurs, kurs, 20, 1, "TESTSYM")) c.commit() def _ergebnis(db): with sqlite3.connect(db) as c: return c.execute("SELECT ret30,hit30,ret60,hit60,done " "FROM rec_outcomes").fetchone() def test_long_auswertung_beide_horizonte(engine, db_umleiten): db = db_umleiten t0 = int(time.time()) - 7200 # beide Horizonte fällig _setze(db, ts=t0, signal="LONG", price=75.00, atr=0.20, kerzen=((30, 75.10), (60, 74.90))) engine._evaluate_rec_outcomes() ret30, hit30, ret60, hit60, done = _ergebnis(db) assert round(ret30, 3) == 0.5 and hit30 == 1 # +0,10 / 0,20 ATR assert round(ret60, 3) == -0.5 and hit60 == 0 assert done == 1 def test_short_ist_spiegelbildlich(engine, db_umleiten): db = db_umleiten t0 = int(time.time()) - 7200 _setze(db, ts=t0, signal="SHORT", price=75.00, atr=0.20, kerzen=((30, 74.90), (60, 74.90))) engine._evaluate_rec_outcomes() ret30, hit30, *_ = _ergebnis(db) assert round(ret30, 3) == 0.5 and hit30 == 1 # fallender Kurs = Treffer def test_fehlende_bar_bleibt_offen(engine, db_umleiten): """⚠ Lieber unbewertet als falsch bewertet: fehlt die Bar (Wochenende, Broker-Ausfall), darf die Zeile NICHT mit einem Nachbarwert gefüllt werden.""" db = db_umleiten t0 = int(time.time()) - 7200 _setze(db, ts=t0, signal="SHORT", price=75.00, atr=0.20, kerzen=((30, 74.90),)) # für 60 min bewusst keine engine._evaluate_rec_outcomes() ret30, hit30, ret60, _, done = _ergebnis(db) assert round(ret30, 3) == 0.5 and hit30 == 1 assert ret60 is None assert not done # ── Aggregation ────────────────────────────────────────────────────────── def test_rec_accuracy_rechnet_richtig(engine, db, history): with sqlite3.connect(db) as c: for ret, hit in ((0.5, 1), (0.3, 1), (-0.4, 0), (-0.2, 0)): c.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price," "atr,ret30,hit30,ret60,hit60,done) " "VALUES (?,?,?,?,?,?,?,?,?,?,1)", (int(time.time()), "LONG", 70, "M5", 75.0, 0.2, ret, hit, ret, hit)) c.commit() a = history.rec_accuracy(200) assert a["n"] == 4 assert a["wr30"] == 50.0 # ⚠ gegen 50 % lesen, nicht gegen 0 assert a["avg30"] == 0.05