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:
Axel Hocks
2026-08-12 20:29:22 +02:00
co-authored by Claude Opus 5
parent 19ee379f20
commit d8839ef7c5
5 changed files with 369 additions and 2 deletions
+91
View File
@@ -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"]))