diff --git a/CLAUDE.md b/CLAUDE.md index 791efd2..7bed041 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -259,6 +259,33 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Anteil, WR-Verfall, PF<1, Einzelverlust). **Wöchentlich** laufen lassen (B4/B5). - **Vor „fertig":** immer `python -m py_compile `; JS grob via Klammern- Balance prüfen (kein node im Env). +- **⚠⚠ TEST-SUITE (`pytest`, `tests/`, seit 2026-08-07) — 13 Tests, ~1 s.** + Im Projekt sind über Monate **dutzende Szenario-Tests** entstanden („mit 10 + Szenarien getestet", „mit 18 synthetischen Szenarien"), alle als + Wegwerf-Skripte. **Keiner davon lief je ein zweites Mal.** Bei einem System, das + man vor jeder Änderung neu starten und live verifizieren muss, ist das der + teuerste Teil der Schleife. + Aufruf: **`python -m pytest`** (Konfiguration in `pytest.ini`). + Erfasst sind zunächst die beiden jüngsten Pfade: **`_check_pending_fill`** + (6 Fälle, u. a. „fremde Position NICHT taggen" und „derselbe Fill zählt nur + einmal") und die **Selbst-Kalibrierung** (7 Fälle: Episoden-Dedup, WARTEN + re-armt, Auswertung in beide Richtungen inkl. Broker-Offset, fehlende Bar bleibt + offen, Aggregation). + ⚠ **Drei harte Regeln in `tests/conftest.py`**, damit die Suite jederzeit + gefahrlos läuft: **keine Live-DB** (jeder Test bekommt eine frische Temp-Datei), + **kein MT5, kein laufender Server** (alles Broker-Nahe gestubbt), **kein Netz**. + ⚠ Das Schema wird über die **echten** Konstruktoren angelegt — `HistoryLogger` + UND `CandleLogger` (`candles_m1` gehört nicht zum History-Schema). Ein von Hand + nachgebautes Test-Schema würde irgendwann vom Produktivstand abweichen. + ⚠ **Warum die Temp-DB und nicht eine Kopie der Live-DB:** am 06.08. haben ECHTE + `candles_m1`-Zeilen in einem vermeintlich leeren Fenster einen Test verfälscht — + zweimal hintereinander sah es nach einem Code-Fehler aus, es waren Testfehler. + ✅ **Mutations-geprüft** (eine Suite, die immer grün ist, ist wertlos): eine + absichtlich eingebaute „fremde Position wird doch getaggt"-Regression wird + gefangen. ⚠ Nebenbefund dabei: der Fill-Dedup ist **doppelt** gesichert + (`_pending_tagged` UND der `pop` aus `_pending_tickets`) — nur eines von beiden + zu entfernen fällt nicht auf, beides zusammen bricht sofort zwei Tests. Das ist + gewollte Redundanz, kein ungetesteter Zweig. - **⚠⚠ KONFIDENZINTERVALLE statt Bauchgefühl (`backtest_cost_gate.ci95`, seit 2026-08-07).** Das Projekt entschied bis dahin über **Punktschätzungen** plus die 2-Stichproben-Regel; ob ein ØR von +0,12 bei n=30 überhaupt von null zu diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e59d2bf --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +addopts = -q --strict-markers diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5701282 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,95 @@ +"""Gemeinsame Fixtures für die Szenario-Tests. + +⚠⚠ DREI HARTE REGELN, damit diese Suite jederzeit gefahrlos läuft: + 1. **Nie die Live-DB anfassen.** Jeder Test bekommt eine frische Temp-Datei; + `HistoryLogger.__init__` ruft `_init_db()` und legt das komplette Schema an. + Die 15,7-MB-Live-DB zu kopieren wäre nicht nur langsam, sondern auch + unzuverlässig — echte Zeilen haben mir am 06.08. einen Test verfälscht + (echte `candles_m1` im vermeintlich leeren Fenster). + 2. **Kein MT5, kein laufender Server.** Alles, was den Broker berührt, wird + gestubbt. Die Tests prüfen die LOGIK, nicht die Anbindung. + 3. **Keine Netzwerkzugriffe.** + +Die Engine wird über `__new__` ohne `__init__` erzeugt: der echte Konstruktor +baut MT5-Verbindung, Threads und Config auf. Für die geprüften Methoden reichen +wenige Attribute — was fehlt, fällt im Test sofort als AttributeError auf und ist +damit dokumentiert, statt still zu passieren. +""" +import sys +import types +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from core.candle_logger import CandleLogger # noqa: E402 +from core.engine import TradingEngine # noqa: E402 +from core.history import HistoryLogger # noqa: E402 + + +@pytest.fixture +def db(tmp_path): + """Frische, vollständig angelegte Test-Datenbank. + + ⚠ `candles_m1` gehört NICHT zum `HistoryLogger`-Schema, sondern wird vom + `CandleLogger` angelegt (`core/candle_logger.py`). Beide werden hier über + ihre ECHTEN Konstruktoren aufgerufen — ein von Hand nachgebautes Test-Schema + würde irgendwann vom Produktivstand abweichen, und der Test prüfte dann etwas + anderes als das, was live läuft. + """ + p = tmp_path / "test.db" + HistoryLogger(p) # legt trades, recommendations, rec_outcomes, … + CandleLogger(str(p)) # legt candles_m1 + return p + + +@pytest.fixture +def history(db): + return HistoryLogger(db) + + +class FakeTrader: + """Minimaler Trader-Ersatz: liefert genau den Snapshot, den der Test setzt.""" + + def __init__(self): + self.snap: dict = {} + self.offset = 3 * 3600 + + def snapshot(self): + return self.snap + + def _broker_offset_s(self, sym=None): + return self.offset + + +@pytest.fixture +def engine(history): + """Nackte Engine ohne `__init__` — nur die Attribute, die geprüft werden.""" + e = TradingEngine.__new__(TradingEngine) + e.history = history + e.trader = FakeTrader() + e.data = types.SimpleNamespace(symbol="XTIUSD") + e.cfg = {"telegram": {"enabled": "false"}} + # Zustand der geprüften Pfade + e._pending_tickets = {} + e._pending_tagged = set() + e._bot_open_ticket = None + e._bot_open_source = None + e._squeeze_entry_count = 0 + e._signal_entry_count = 0 + e._rec_epi_sig = None + e._rec_eval_ts = 0.0 + return e + + +@pytest.fixture +def db_umleiten(db, monkeypatch): + """Leitet das modulweite `HISTORY_DB_FILE` der Engine auf die Test-DB um. + + ⚠ Nötig, weil `_evaluate_rec_outcomes` eine EIGENE sqlite3-Verbindung öffnet + (bewusst — die Auswertung läuft außerhalb des HistoryLogger-Locks). + """ + import core.engine as CE + monkeypatch.setattr(CE, "HISTORY_DB_FILE", str(db)) + return db diff --git a/tests/test_pending_fill.py b/tests/test_pending_fill.py new file mode 100644 index 0000000..397a2ae --- /dev/null +++ b/tests/test_pending_fill.py @@ -0,0 +1,74 @@ +"""`engine._check_pending_fill` — ordnet eine gefüllte Stop-Order dem Bot zu. + +HINTERGRUND (2026-08-06): eine ruhende Stop-Order füllt IM BROKER, nicht über +`engine._open`. `trader._refresh_locked` adoptiert die Position dann per +magic-match und loggt sie OHNE `setup` — vier reale Squeeze-Trades waren dadurch +von manuellen nicht unterscheidbar, und B4-Monitor, `squeeze_b5` sowie +`squeeze_entry_gap` zählten **null**. Deployment-Drift Fall 8: nicht die Strategie +driftete, sondern ihre Beobachtbarkeit. + +Die Zuordnung ist eindeutig, weil bei einem Pending-Fill die POSITIONS-Nummer +identisch mit der ORDER-Nummer ist (an allen vier Broker-Datensätzen verifiziert). +""" + + +def test_squeeze_fill_wird_getaggt(engine): + engine._pending_tickets = {49483981: "squeeze"} + engine.trader.snap = {"ticket": 49483981, "order_type": 1, + "entry_price": 75.127} + engine._check_pending_fill() + + assert engine._squeeze_entry_count == 1 + assert engine._bot_open_ticket == 49483981 + assert engine._bot_open_source == "auto_squeeze" + assert 49483981 not in engine._pending_tickets # Merker verbraucht + + +def test_signal_fill_long(engine): + engine._pending_tickets = {777: "signal"} + engine.trader.snap = {"ticket": 777, "order_type": 0, "entry_price": 75.2} + engine._check_pending_fill() + + assert engine._signal_entry_count == 1 + assert engine._squeeze_entry_count == 0 + assert engine._bot_open_source == "auto_signal" + + +def test_fremde_position_wird_nicht_getaggt(engine): + """Der gefährliche Fall: ein manueller Trade darf NICHT als Bot-Trade gelten — + sonst verfälscht er genau die Monitore, die der Fix reparieren soll.""" + engine._pending_tickets = {111: "squeeze"} + engine.trader.snap = {"ticket": 999, "order_type": 0, "entry_price": 75.0} + engine._check_pending_fill() + + assert engine._squeeze_entry_count == 0 + assert engine._bot_open_ticket is None + assert engine._pending_tickets == {111: "squeeze"} # bleibt liegen + + +def test_derselbe_fill_zaehlt_nur_einmal(engine): + """Der `_pos_loop` läuft im Sekundentakt — ohne Dedup zählte ein Trade + hundertfach und der B4-Monitor wäre wertlos.""" + engine._pending_tickets = {222: "squeeze"} + engine.trader.snap = {"ticket": 222, "order_type": 1, "entry_price": 74.9} + for _ in range(5): + engine._check_pending_fill() + + assert engine._squeeze_entry_count == 1 + + +def test_flat_ist_ein_no_op(engine): + engine._pending_tickets = {333: "squeeze"} + engine.trader.snap = {"ticket": None} + engine._check_pending_fill() + + assert engine._squeeze_entry_count == 0 + assert engine._pending_tickets == {333: "squeeze"} + + +def test_ohne_pending_merker_sofort_raus(engine): + """Der häufigste Fall — es darf keine DB-Arbeit anfallen.""" + engine.trader.snap = {"ticket": 444, "order_type": 0} + engine._check_pending_fill() + + assert engine._bot_open_ticket is None diff --git a/tests/test_rec_outcomes.py b/tests/test_rec_outcomes.py new file mode 100644 index 0000000..47b64fd --- /dev/null +++ b/tests/test_rec_outcomes.py @@ -0,0 +1,133 @@ +"""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 diff --git a/tests_rec_outcomes.py b/tests_rec_outcomes.py deleted file mode 100644 index 2c4e7ae..0000000 --- a/tests_rec_outcomes.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Szenario-Test der Selbst-Kalibrierung (rec_outcomes) — ohne MT5, auf DB-Kopie. - -Geprueft werden die Faelle, die live schiefgehen koennen: - 1) Episode wird EINMAL geloggt, nicht je Minute - 2) WARTEN re-armt → die naechste Richtung ist eine neue Episode - 3) Auswertung rechnet den Forward-Return korrekt (inkl. Broker-Offset) - 4) fehlende M1-Bars → Zeile bleibt offen statt falsch bewertet - 5) rec_accuracy aggregiert Trefferquote und Ø-Return richtig -""" -import os -import shutil -import sqlite3 -import sys -import tempfile -import time -import types - -sys.path.insert(0, r"c:\Users\ah\Downloads\tradingrobot") - -from core.engine import TradingEngine -from core.history import HistoryLogger - -QUELL = r"c:\Users\ah\Downloads\tradingrobot\oil_widget_history.db" -TMP = os.path.join(tempfile.mkdtemp(), "t.db") -shutil.copy(QUELL, TMP) -con = sqlite3.connect(TMP) -con.execute("DELETE FROM rec_outcomes") -con.commit() - -h = HistoryLogger(TMP) -fehler = [] - - -def pruef(lbl, ist, soll): - ok = ist == soll - if not ok: - fehler.append(lbl) - print(f" {'OK ' if ok else 'FEHLER'} {lbl:<50} ist={ist!r:<14} soll={soll!r}") - - -print("=" * 86) -print(" 1) Episoden-Logging: eine Zeile je Richtungswechsel, nicht je Minute") -print("=" * 86) - - -class E: - """Nur der Episoden-Teil aus `_run_analysis`, isoliert nachgestellt.""" - def __init__(self): - self._rec_epi_sig = None - self.history = h - - def tick(self, sig): - if sig in ("LONG", "SHORT") and sig != self._rec_epi_sig: - self.history.log_rec_outcome(signal=sig, conf_pct=70, tf="M5", - price=75.0, atr=0.2) - if sig in ("LONG", "SHORT", "WARTEN"): - self._rec_epi_sig = sig if sig != "WARTEN" else None - - -e = E() -for s in ["LONG"] * 5: # dieselbe Episode, 5 Minuten lang - e.tick(s) -pruef("5x LONG hintereinander → 1 Zeile", - con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 1) -for s in ["SHORT"] * 3: # Flip = neue Episode - e.tick(s) -pruef("Flip auf SHORT → 2 Zeilen", - con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 2) -e.tick("WARTEN"); e.tick("WARTEN") -pruef("WARTEN erzeugt keine Zeile", - con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 2) -e.tick("SHORT") -pruef("nach WARTEN ist SHORT eine NEUE Episode", - con.execute("SELECT COUNT(*) FROM rec_outcomes").fetchone()[0], 3) - -print() -print("=" * 86) -print(" 2) Auswertung gegen candles_m1 (mit Broker-Offset)") -print("=" * 86) -con.execute("DELETE FROM rec_outcomes") -con.execute("DELETE FROM candles_m1 WHERE symbol='TESTSYM'") -OFF = 3 * 3600 -t0 = int(time.time()) - 7200 # vor 2 h → beide Horizonte faellig -# LONG @ 75.00, ATR 0.20; nach 30 min steht 75.10 (+0,5 ATR), nach 60 min 74.90 -con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) " - "VALUES (?,?,?,?,?,?)", (t0, "LONG", 70, "M5", 75.00, 0.20)) -for minuten, kurs in ((30, 75.10), (60, 74.90)): - con.execute("INSERT OR REPLACE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol)" - " VALUES (?,?,?,?,?,?,?,?)", - (t0 + minuten * 60 + OFF, kurs, kurs, kurs, kurs, 20, 1, "TESTSYM")) -con.commit() - -eng = TradingEngine.__new__(TradingEngine) -eng._rec_eval_ts = 0.0 -eng.trader = types.SimpleNamespace(_broker_offset_s=lambda: OFF) -import core.engine as CE -_alt = CE.HISTORY_DB_FILE -CE.HISTORY_DB_FILE = TMP -eng._evaluate_rec_outcomes() -CE.HISTORY_DB_FILE = _alt - -r = con.execute("SELECT ret30,hit30,ret60,hit60,done FROM rec_outcomes").fetchone() -pruef("ret30 = (75.10-75.00)/0.20 = +0.5", round(r[0], 3), 0.5) -pruef("hit30 = 1 (lief in Signalrichtung)", r[1], 1) -pruef("ret60 = (74.90-75.00)/0.20 = -0.5", round(r[2], 3), -0.5) -pruef("hit60 = 0", r[3], 0) -pruef("done = 1", r[4], 1) - -print() -print("=" * 86) -print(" 3) SHORT-Richtung spiegelbildlich · fehlende Bars → bleibt offen") -print("=" * 86) -con.execute("DELETE FROM rec_outcomes") -# ⚠ Test-Kerzen aus Abschnitt 2 MUESSEN weg — sonst faellt die dortige -# 60-Minuten-Bar in das Fenster dieses Abschnitts (beide Anker liegen 2 h -# zurueck) und der Fall "keine Bar" waere gar nicht getestet. -con.execute("DELETE FROM candles_m1 WHERE symbol='TESTSYM'") -t1 = int(time.time()) - 7200 -con.execute("INSERT INTO rec_outcomes (ts,signal,conf_pct,tf,price,atr) " - "VALUES (?,?,?,?,?,?)", (t1, "SHORT", 70, "M5", 75.00, 0.20)) -con.execute("INSERT OR REPLACE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol)" - " VALUES (?,?,?,?,?,?,?,?)", - (t1 + 1800 + OFF, 74.9, 74.9, 74.9, 74.9, 20, 1, "TESTSYM")) -# fuer 60 min BEWUSST keine Bar → Zeile darf nicht faelschlich bewertet werden. -# ⚠ Dafuer muss das 60-Minuten-Fenster WIRKLICH leer sein: die DB-Kopie enthaelt -# ECHTE candles_m1 aus dem Live-Betrieb, und die liegen genau hier. Ohne dieses -# DELETE testet man nicht "keine Bar", sondern "echte Bar" (erster Versuch lieferte -# ret60 = -14,985 = der reale Kurs ~78). -con.execute("DELETE FROM candles_m1 WHERE time > ? AND time <= ?", - (t1 + 3600 + OFF - 900, t1 + 3600 + OFF)) -con.commit() -eng._rec_eval_ts = 0.0 -CE.HISTORY_DB_FILE = TMP -eng._evaluate_rec_outcomes() -CE.HISTORY_DB_FILE = _alt -r = con.execute("SELECT ret30,hit30,ret60,done FROM rec_outcomes").fetchone() -pruef("SHORT: Kurs faellt → ret30 = +0.5", round(r[0], 3), 0.5) -pruef("SHORT: hit30 = 1", r[1], 1) -pruef("ret60 ohne Bar bleibt NULL", r[2], None) -pruef("done bleibt offen (0/None)", bool(r[3]), False) - -print() -print("=" * 86) -print(" 4) rec_accuracy aggregiert") -print("=" * 86) -con.execute("DELETE FROM rec_outcomes") -for ret, hit in ((0.5, 1), (0.3, 1), (-0.4, 0), (-0.2, 0)): - con.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)) -con.commit() -a = h.rec_accuracy(200) -pruef("n = 4", a["n"], 4) -pruef("Trefferquote 30' = 50 %", a["wr30"], 50.0) -pruef("Ø-Return 30' = +0.05", a["avg30"], 0.05) - -con.close() -print() -print("=" * 86) -print(" ERGEBNIS: " + ("alle Szenarien korrekt" if not fehler - else f"{len(fehler)} FEHLER: {fehler}")) -print("=" * 86) -sys.exit(1 if fehler else 0)