From d8839ef7c589725cc80d8cb462577b95f52df83b Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Wed, 12 Aug 2026 20:29:22 +0200 Subject: [PATCH] Tag P/L war falsch: Fehlbuchungen beim Schliessen behoben + repariert User-Frage "ueberpruefe ob Tag P/L richtig berechnet wird". Die Formel (realisiert + offen) war bitgenau richtig - die Datenbasis nicht: Anzeige +14,26 EUR gegen -74,08 EUR realisiert beim Broker. Ursache, im Log im Sekundentakt (#49926460): 20:04:10 eroeffnet -> 20:04:10 "extern geschlossen" -> "kein OUT-Deal in 1 Deals" -> Fallback bucht 0,00 -> 20:11:42 der echte Close lief ins Leere. Drei Defekte: (1) positions_get sieht die frische Position einen Tick lang nicht, (2) _log_external_close buchte TROTZ "kein OUT-Deal" einen Close - dabei ist genau das der Beweis, dass sie noch offen ist, (3) log_trade_close fasste nur exit_time IS NULL an, die Fehlbuchung blockierte den echten Close dauerhaft. Bei #49852362 kostete das +5,87 statt -95,38 EUR und den falschen Tag. Behoben: (1) Abbruch statt Fallback bei "kein OUT-Deal"; (2) eine erkennbare Fehlbuchung (closed_by='unknown') darf von einem echten Close korrigiert werden - eng gefasst, gute Zeilen bleiben unberuehrt. 4 Tests inkl. Gegenprobe. Altlast: tools/repair_closes.py (Trockenlauf Standard, Backup automatisch). 18 Zeilen ueber 60 Tage korrigiert. Heute von +7,63 auf -72,67 (Restfehler 1,41). Gesamt-P&L von -282,78 auf +169,98. Zeitfalle zweimal getroffen: history_deals_get filtert nach Broker-Wallclock, und fromtimestamp(d.time, BROKER) rendert 3 h zu spaet. Aufgefallen nur, weil eine Deal-Zeit 23:11 lautete, das Log aber 20:11:42 sagte. Statistik-Modul separat geprueft: Arithmetik in allen drei Zeitraeumen bitgenau korrekt (Abweichung 0,00). Der Netto-Defekt vom 05.08. besteht weiter und ist groesser als damals: angezeigt -2.449,95 EUR, real verblieben -17,06 (99 % der Steuer werden erstattet). Nicht gebaut - die Loesung ist dokumentiert. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 73 +++++++++++++++++ core/history.py | 21 ++++- core/trader.py | 27 +++++- tests/test_close_buchung.py | 91 +++++++++++++++++++++ tools/repair_closes.py | 159 ++++++++++++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 tests/test_close_buchung.py create mode 100644 tools/repair_closes.py diff --git a/CLAUDE.md b/CLAUDE.md index 6639686..d5867b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6161,6 +6161,79 @@ Neumessung. Sie bleiben unverändert und reproduzierbar; wo ihre Schlussfolgerun relevant wird, gehört sie live-treu **neu gerechnet** (Muster: `backtest_auto_signal_norev.py`). +## ⚠⚠ TAG P/L WAR FALSCH — Fehlbuchungen beim Schliessen (2026-08-12) + +User: „überprüfe ob Tag P/L richtig berechnet wird." **Die Formel war richtig, die +Datenbasis nicht.** +| | | +|---|---| +| Anzeige „TAG P/L" | **+14,26 €** | +| Broker, realisiert heute | **−74,08 €** | +| **Abweichung** | **81,71 €, mit falschem Vorzeichen** | +✅ `realisiert + offen` stimmt bitgenau gegen die DB (0,0000). Der Fehler lag +darunter: **die DB stimmte nicht mit dem Broker überein.** + +⚠⚠ **URSACHE — eine Race Condition beim Eröffnen**, im Log im Sekundentakt +sichtbar (#49926460): `20:04:10 SELL eröffnet` → `20:04:10 „extern geschlossen"` +→ `20:04:12 „kein OUT-Deal in 1 Deals"` → Fallback bucht **0,00** → +`20:11:42` der ECHTE Close **lief ins Leere**. Drei Defekte griffen ineinander: +**(1)** `positions_get` sieht die frische Position einen Tick lang nicht. +**(2)** `_log_external_close` buchte **trotz** „kein OUT-Deal" einen Close — dabei +ist genau das der Beweis, dass die Position NOCH OFFEN ist. +**(3)** `log_trade_close` fasste nur `exit_time IS NULL` an → die Fehlbuchung +blockierte den echten Close **dauerhaft**. +Bei #49852362 kostete das **+5,87 statt −95,38 €** und buchte den Trade auf den +**falschen Tag**. + +✅ **BEHOBEN:** (1) `_log_external_close` bricht bei „kein OUT-Deal" ab (der +Fallback bleibt nur für den wirklich blinden Fall, in dem gar keine Deals +abrufbar sind — dort ist `reconcile_open_trades` das Netz). (2) `log_trade_close` +darf eine **erkennbare Fehlbuchung** korrigieren — eng gefasst: nur wenn die +Zeile `closed_by='unknown'` trägt UND die neue Buchung echte Deal-Daten hat. +✅ **4 Tests** (`tests/test_close_buchung.py`), inkl. der Gegenprobe „eine gute +Zeile wird NICHT überschrieben" — sonst wäre der Fix schlimmer als der Fehler. +⚠ Zwei Testfehlschläge waren **meine**: synthetische Zeitstempel (1000/2000) vor +der Eröffnungszeit lösten die Defensive „exit < entry → jetzt" aus. + +✅ **ALTLAST REPARIERT** — `tools/repair_closes.py` (Trockenlauf ist Standard, +Schreiben nur mit `--schreiben`, Backup `…bak-2026-08-12-repair`). Korrigiert +werden **nur eindeutige Fälle**: `closed_by='unknown'` UND echter OUT-Deal beim +Broker. **18 Zeilen** über 60 Tage. Heute: von **+7,63 auf −72,67 €**, Restfehler +**1,41 €**. Gesamt-P&L (`all`): von **−282,78 auf +169,98 €**. +⚠⚠ **ZEITFALLE dabei, zweimal:** `history_deals_get` filtert nach BROKER-Wallclock +— `datetime`-Objekte verschieben das Fenster; und `fromtimestamp(d.time, BROKER)` +rendert **3 h zu spät**. Aufgefallen, weil eine Deal-Zeit „23:11" lautete, das Log +aber „20:11:42 CLOSE" sagte. ➜ **Epochs übergeben (lokal + Offset), beim Lesen +`d.time − Offset`.** +⚠ **20 weitere Trades** weichen um ±1–4 € ab (DB nutzt den Schwebe-P&L im +Moment des Closes, der Broker den Fill). Die Summe hebt sich fast auf (+1,66 € +über 19 Trades) → nur berichtet, **nicht** automatisch korrigiert. + +## ✅ STATISTIK-MODUL: ARITHMETIK KORREKT, NETTO-RECHNUNG WEITER DEFEKT (2026-08-12) + +Gegen unabhängige SQL geprüft, alle drei Zeiträume: **Abweichung 0,00 in jeder +Zelle** (n · wins+losses+be = n · gross_win − gross_loss = total_pnl). +| | today | week | all | +|---|---|---|---| +| n | 6 | 91 | 1.267 | +| total_pnl | −72,67 | −1.259,12 | **+169,98** | +✅ Das Modul rechnet also korrekt — es las nur eine fehlerhafte DB (jetzt repariert). + +⚠⚠ **Der am 05.08. dokumentierte NETTO-Defekt besteht unverändert — und ist +grösser als damals gemessen:** +| | | +|---|---| +| Modell-Steuerlast (`gross_win × 26,375 %`) | **2.619,93 €** | +| **angezeigtes `net_pnl`** | **−2.449,95 €** | +| ECHT einbehalten / erstattet (90 Tage) | −2.625,89 / **+2.608,83** | +| **tatsächlich verblieben** | **−17,06 €** | +✅ Das Modell schätzt die *einbehaltene* Steuer fast perfekt (2.620 gegen 2.626). +Der Fehler ist die Annahme, sie sei **verloren** — real werden **99 %** erstattet +(am 05.08. waren es 79 %). Die Anzeige macht damit aus **+153 € rund −2.450 €**. +⚠ `_add_net`/`_add_net_setup` in `server.py:154/171` sind unverändert. Die Lösung +steht seit dem 05.08. fest: **die tatsächlichen `WHT #…`/`Tax settlement`-Deals +verwenden statt zu schätzen** (gecacht, unter `mt5_lock`). **Noch nicht gebaut.** + ## ✅ AUTO-SIGNAL-ENTRY (🎯 SIG) KOMPLETT ENTFERNT (User-Entscheidung 2026-08-12) User: „den Signal-Autotrader brauchen wir im Moment nicht mehr — sollten wir den diff --git a/core/history.py b/core/history.py index dd44f28..d1b49a3 100644 --- a/core/history.py +++ b/core/history.py @@ -395,12 +395,31 @@ class HistoryLogger: ).fetchone() if row and row["entry_time"] and ts < row["entry_time"]: ts = int(time.time()) - conn.execute(""" + cur = conn.execute(""" UPDATE trades SET exit_time = ?, exit_price = ?, pnl = ?, closed_by = ?, commission = ? WHERE ticket = ? AND exit_time IS NULL """, (ts, exit_price, pnl, closed_by, commission, ticket)) + # ⚠⚠ KORREKTUR EINER FEHLBUCHUNG (2026-08-12). Bis dahin galt strikt + # `exit_time IS NULL` — eine einmal geschriebene Zeile war endgueltig. + # Das machte einen Folgefehler dauerhaft: buchte der Fallback-Pfad + # eine Position faelschlich als geschlossen (real #49852362: + # +5,87 statt −95,38 € und auf dem falschen Tag), lief der SPAETERE, + # ECHTE Close still ins Leere. + # ⚠ ENG GEFASST, damit keine gute Zeile ueberschrieben wird: + # korrigiert wird NUR, wenn die bestehende Zeile eine erkennbare + # Fehlbuchung ist (`closed_by='unknown'`) UND die neue Buchung + # echte Deal-Daten mitbringt (`closed_by != 'unknown'`). + if cur.rowcount == 0 and closed_by != "unknown": + cur = conn.execute(""" + UPDATE trades + SET exit_time = ?, exit_price = ?, pnl = ?, + closed_by = ?, commission = ? + WHERE ticket = ? AND closed_by = 'unknown' + """, (ts, exit_price, pnl, closed_by, commission, ticket)) + if cur.rowcount: + self._korrigiert = getattr(self, "_korrigiert", 0) + 1 conn.commit() # KEIN Trade-Abschluss-Telegram mehr (User-Vorgabe): Telegram zum Thema # Schließen kommt nur noch beim echten Signal-Flip (engine._check_close_ diff --git a/core/trader.py b/core/trader.py index 25e971a..b0e92a1 100644 --- a/core/trader.py +++ b/core/trader.py @@ -779,9 +779,34 @@ class TradeManager: f"Externer Close: T={ticket} {closed_by} @ " f"{close_deal.price:.3f} pnl={total_profit:.2f}") return - log_hist.warning(f"T={ticket}: kein OUT-Deal in {len(own)} Deals") + # ⚠⚠ HIER LAG DER FEHLER (behoben 2026-08-12). Vorher fiel dieser + # Zweig in den Fallback durch und buchte den zuletzt bekannten + # SCHWEBE-Gewinn als realisierten Close. + # Das ist genau falsch herum: wir haben die Deals der Position + # GEFUNDEN und festgestellt, dass **kein OUT-Deal existiert**. + # Das ist positive Evidenz, dass die Position NOCH OFFEN ist — + # typischerweise ein transient leeres `positions_get` direkt + # nach dem Eröffnen. + # Real am 12.08.: #49926460 wurde 0 Sekunden nach dem Öffnen als + # „extern geschlossen" mit pnl 0,00 und exit_price 0,0 gebucht; + # der ECHTE Close 7 min später lief ins Leere, weil + # `log_trade_close` nur Zeilen mit `exit_time IS NULL` anfasst. + # Bei #49852362 kostete dasselbe **+5,87 statt −95,38 €** und + # buchte den Trade auf den FALSCHEN TAG. + # ➜ Nicht schreiben. Der nächste Tick prüft erneut; bleibt die + # Position wirklich weg, liefert der Broker dann einen + # OUT-Deal. `reconcile_open_trades` ist das zweite Netz. + log_hist.warning( + f"T={ticket}: kein OUT-Deal in {len(own)} Deals — Position gilt " + f"als WEITER OFFEN, kein Close gebucht (Fallback uebersprungen)") + return # ── Methode 3: Fallback — letzter bekannter PnL ─────────────────── + # ⚠ Nur noch fuer den WIRKLICH blinden Fall: es liess sich KEIN + # einziger Deal der Position abrufen (MT5-Aussetzer, Broker setzt + # `position_id` nicht). Dann ist „offen oder zu" nicht entscheidbar, + # und eine Zeile mit `closed_by='unknown'` ist besser als ein Trade, + # der ewig offen in der DB haengt. if fallback_pnl is not None: self.history.log_trade_close( ticket=ticket, diff --git a/tests/test_close_buchung.py b/tests/test_close_buchung.py new file mode 100644 index 0000000..a03d384 --- /dev/null +++ b/tests/test_close_buchung.py @@ -0,0 +1,91 @@ +"""Fehlbuchung beim Schliessen: darf nicht entstehen — und muss korrigierbar sein. + +ANLASS (2026-08-12, User: „ueberpruefe ob Tag P/L richtig berechnet wird"). +Ergebnis: die FORMEL war richtig, die DATENBASIS nicht — Anzeige +14,26 EUR +gegen −74,08 EUR realisiert beim Broker. + +⚠⚠ URSACHE, im Log im Sekundentakt sichtbar (#49926460): + 20:04:10 SELL 1.64L eroeffnet + 20:04:10 „Position extern geschlossen" <- 0 Sekunden spaeter + 20:04:12 „kein OUT-Deal in 1 Deals" + 20:04:12 Fallback-PnL 0,00 gebucht + 20:11:42 der ECHTE Close — lief ins Leere +Drei Defekte griffen ineinander: + (1) `positions_get` sieht die frische Position einen Tick lang nicht, + (2) `_log_external_close` bucht TROTZ „kein OUT-Deal" einen Close, + (3) `log_trade_close` fasst nur `exit_time IS NULL` an → die Fehlbuchung + blockiert den echten Close dauerhaft. +Bei #49852362 kostete das **+5,87 statt −95,38 EUR** und buchte den Trade auf +den FALSCHEN TAG. +""" +from __future__ import annotations + +import time + +import pytest + +# Zeitstempel MUESSEN nach der Eroeffnung liegen: `log_trade_close` schiebt +# ein exit VOR dem entry defensiv auf `jetzt` (sonst waere die Haltedauer +# negativ). Mit 1000/2000 misst man diese Defensive, nicht den Fix. +_T1 = int(time.time()) + 10 +_T2 = int(time.time()) + 20 + + +def test_fehlbuchung_wird_von_echtem_close_korrigiert(history): + """Defekt (3): eine `unknown`-Zeile darf der Wahrheit nicht im Weg stehen.""" + history.log_trade_open(ticket=4711, symbol="SpotCrude", direction="SELL", + lots=1.64, entry_price=83.162) + # Fehlbuchung, wie der Fallback sie schrieb + history.log_trade_close(ticket=4711, exit_price=0.0, pnl=0.0, + closed_by="unknown", exit_ts=_T1) + # der ECHTE Close mit Deal-Daten + history.log_trade_close(ticket=4711, exit_price=83.056, pnl=15.08, + closed_by="manual", exit_ts=_T2) + + t = history.trade_by_ticket(4711) + assert t["pnl"] == 15.08, "der echte Close muss die Fehlbuchung ersetzen" + assert t["closed_by"] == "manual" + assert t["exit_price"] == 83.056 + assert t["exit_time"] == _T2, "auch der TAG muss korrigiert werden" + + +def test_gute_zeile_wird_NICHT_ueberschrieben(history): + """⚠ Die Gegenprobe — sonst waere der Fix schlimmer als der Fehler. + Ein sauber gebuchter Close ist endgueltig.""" + history.log_trade_open(ticket=4712, symbol="SpotCrude", direction="BUY", + lots=1.0, entry_price=80.0) + history.log_trade_close(ticket=4712, exit_price=81.0, pnl=100.0, + closed_by="sl", exit_ts=_T1) + history.log_trade_close(ticket=4712, exit_price=99.9, pnl=-999.0, + closed_by="manual", exit_ts=_T2) + + t = history.trade_by_ticket(4712) + assert t["pnl"] == 100.0 and t["closed_by"] == "sl" and t["exit_time"] == _T1 + + +def test_unknown_ersetzt_nicht_unknown(history): + """Eine zweite Fehlbuchung darf die erste nicht ueberschreiben — sonst + wandert der Zeitstempel bei jedem Tick weiter und der Trade landet + irgendwann auf einem beliebigen Tag.""" + history.log_trade_open(ticket=4713, symbol="SpotCrude", direction="BUY", + lots=1.0, entry_price=80.0) + history.log_trade_close(ticket=4713, exit_price=0.0, pnl=0.0, + closed_by="unknown", exit_ts=_T1) + history.log_trade_close(ticket=4713, exit_price=0.0, pnl=5.0, + closed_by="unknown", exit_ts=_T2) + + t = history.trade_by_ticket(4713) + assert t["exit_time"] == _T1 and t["pnl"] == 0.0 + + +def test_offener_trade_bleibt_offen_ohne_close(history): + """Grundinvariante: ohne Close-Buchung keine exit_time — sonst zaehlt ein + laufender Trade in den Tages-P&L.""" + history.log_trade_open(ticket=4714, symbol="SpotCrude", direction="BUY", + lots=1.0, entry_price=80.0) + t = history.trade_by_ticket(4714) + assert t["exit_time"] is None and t["pnl"] is None + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-v"])) diff --git a/tools/repair_closes.py b/tools/repair_closes.py new file mode 100644 index 0000000..c340e66 --- /dev/null +++ b/tools/repair_closes.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Fehlgebuchte Trade-Closes gegen die BROKER-Deals korrigieren. + +ANLASS (2026-08-12). Der Tages-P&L zeigte **+14,26 €**, der Broker hatte +**−74,08 €** realisiert. Die Formel war richtig, die DATENBASIS nicht: der +Fallback-Pfad in `trader._log_external_close` buchte Positionen als geschlossen, +obwohl gar kein OUT-Deal vorlag (transient leeres `positions_get` direkt nach dem +Oeffnen). Weil `log_trade_close` nur Zeilen mit `exit_time IS NULL` anfasste, lief +der SPAETERE, echte Close ins Leere. +Beide Ursachen sind im Code behoben — dieses Skript raeumt die Altlast auf. + +⚠⚠ NUR EINDEUTIGE FAELLE. Korrigiert wird ausschliesslich, wo + (a) die DB-Zeile `closed_by='unknown'` traegt (= Fallback-Buchung), UND + (b) der Broker fuer dieselbe Position einen echten OUT-Deal hat. +Alles andere wird nur BERICHTET, nicht angefasst — eine Massenkorrektur auf +Verdacht waere schlimmer als die Altlast. + +⚠ Standard ist der TROCKENLAUF. Schreiben nur mit `--schreiben`, und dann nur +nach einem Backup (`oil_widget_history.db.bak-`), das das Skript selbst +anlegt. + +Aufruf: python tools/repair_closes.py [--tage N] [--schreiben] +""" +from __future__ import annotations + +import argparse +import datetime as dt +import os +import shutil +import sqlite3 +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import MetaTrader5 as mt5 # noqa: E402 + +HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DB = os.path.join(HERE, "oil_widget_history.db") +_BROKER = dt.timezone(dt.timedelta(hours=3)) + + +def broker_outs(tage: int) -> dict: + """{position_id: (exit_ts_lokal, preis, pnl, kommission, grund)} aus den Deals.""" + mt5.initialize() + # ⚠⚠ ZEITFALLE (korrigiert 2026-08-12): `history_deals_get` filtert nach + # BROKER-Wallclock. Wer `datetime`-Objekte uebergibt, verschiebt das + # Fenster um den Offset und verliert die juengsten Deals. Und wer die + # Rueckgabe mit `fromtimestamp(d.time, BROKER)` rendert, sieht sie **3 h + # zu spaet** — real fiel mir das auf, weil eine Deal-Zeit „23:11" lautete, + # das Log aber „20:11:42 CLOSE" sagte. + # ➜ Epochs uebergeben (lokal + Offset), und beim Lesen `d.time - Offset`. + _OFF = 10800 + bis = int(dt.datetime.now().timestamp()) + _OFF + von = bis - tage * 86400 + deals = mt5.history_deals_get(von, bis + 300) + gruende = {} + try: + gruende = {mt5.DEAL_REASON_SL: "sl", mt5.DEAL_REASON_TP: "tp", + mt5.DEAL_REASON_CLIENT: "manual", mt5.DEAL_REASON_EXPERT: "manual", + mt5.DEAL_REASON_MOBILE: "manual", mt5.DEAL_REASON_WEB: "manual"} + except AttributeError: + pass + mt5.shutdown() + if not deals: + return {} + # ⚠ Kommission/Swap fallen auch am IN-Deal an → je Position ALLE Deals summieren. + proj: dict = {} + for d in deals: + pid = getattr(d, "position_id", 0) + if not pid: + continue + p = proj.setdefault(pid, {"pnl": 0.0, "komm": 0.0, "out": None}) + p["pnl"] += d.profit + d.swap + d.commission + p["komm"] += d.commission + if d.entry == mt5.DEAL_ENTRY_OUT: + p["out"] = d + out = {} + for pid, p in proj.items(): + d = p["out"] + if d is None: + continue # Position noch offen → nichts zu tun + # deal.time ist BROKER-Wallclock als Epoch → in lokale Epoch umrechnen + ts = int(d.time) - 10800 + out[pid] = (ts, float(d.price), round(p["pnl"], 2), round(p["komm"], 2), + gruende.get(getattr(d, "reason", None), "unknown")) + return out + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--tage", type=int, default=7) + ap.add_argument("--schreiben", action="store_true") + a = ap.parse_args() + + bro = broker_outs(a.tage) + print(f" Broker: {len(bro)} Positionen mit OUT-Deal in {a.tage} Tagen") + db = sqlite3.connect(DB); db.row_factory = sqlite3.Row + seit = dt.datetime.now().timestamp() - a.tage * 86400 + rows = [dict(r) for r in db.execute( + "SELECT ticket,exit_time,exit_price,pnl,closed_by FROM trades " + "WHERE exit_time IS NOT NULL AND exit_time >= ?", (seit,))] + + fix, hinweis = [], [] + for r in rows: + b = bro.get(r["ticket"]) + if not b: + continue + ts, px, pnl, komm, grund = b + gleich = (abs((r["pnl"] or 0) - pnl) < 0.01 + and abs((r["exit_price"] or 0) - px) < 0.0005) + if gleich: + continue + (fix if r["closed_by"] == "unknown" else hinweis).append((r, b)) + # Zeilen, die der Broker heute schloss, in der DB aber auf einem anderen Tag stehen + for pid, b in bro.items(): + r = db.execute("SELECT ticket,exit_time,exit_price,pnl,closed_by FROM trades " + "WHERE ticket=?", (pid,)).fetchone() + if r and r["closed_by"] == "unknown" and dict(r) not in [x[0] for x in fix]: + d = dict(r) + if abs((d["pnl"] or 0) - b[2]) >= 0.01 and not any(x[0]["ticket"] == pid for x in fix): + fix.append((d, b)) + + def zeig(titel, liste): + print(f"\n {titel} ({len(liste)})") + if not liste: + return + print(f" {'Ticket':<12}{'DB pnl':>10}{'Broker':>10}{'Δ':>10} {'DB Zeit':<14}" + f"{'Broker Zeit':<14}{'by'}") + for r, b in liste: + f = lambda t: dt.datetime.fromtimestamp(t).strftime("%d.%m %H:%M") + print(f" {r['ticket']:<12}{r['pnl'] or 0:>+10.2f}{b[2]:>+10.2f}" + f"{b[2]-(r['pnl'] or 0):>+10.2f} {f(r['exit_time']):<14}" + f"{f(b[0]):<14}{r['closed_by']} → {b[4]}") + + zeig("KORRIGIERBAR (closed_by='unknown' + echter OUT-Deal)", fix) + zeig("⚠ NUR HINWEIS — Abweichung, aber KEINE Fallback-Buchung", hinweis) + + if not a.schreiben: + print(f"\n TROCKENLAUF — nichts geschrieben. Mit `--schreiben` ausfuehren.") + return 0 + if not fix: + print("\n nichts zu korrigieren.") + return 0 + bak = DB + ".bak-" + dt.datetime.now().strftime("%Y-%m-%d-repair") + shutil.copy2(DB, bak) + print(f"\n Backup: {os.path.basename(bak)}") + n = 0 + for r, b in fix: + ts, px, pnl, komm, grund = b + db.execute("UPDATE trades SET exit_time=?, exit_price=?, pnl=?, closed_by=?, " + "commission=? WHERE ticket=?", (ts, px, pnl, grund, komm, r["ticket"])) + n += db.total_changes and 1 + db.commit() + print(f" {len(fix)} Zeilen korrigiert.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())