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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
19ee379f20
commit
d8839ef7c5
@@ -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-<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())
|
||||
Reference in New Issue
Block a user