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,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"]))
|
||||
Reference in New Issue
Block a user