#!/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-<datum>`), 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())
