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
@@ -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
|
||||
|
||||
+20
-1
@@ -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_
|
||||
|
||||
+26
-1
@@ -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,
|
||||
|
||||
@@ -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"]))
|
||||
@@ -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