Netto-Rechnung aus den ECHTEN Steuer-Buchungen statt geschaetzt

Der seit 05.08. dokumentierte Defekt ist behoben. _add_net rechnete
net_pnl = total_pnl - gross_win * 26,375 % und unterstellte, die
Quellensteuer sei endgueltig verloren. Sie wird aber taeglich per
"Tax settlement" erstattet - real zu 99 %.

all: vorher net_pnl -2.449,95 EUR, jetzt +181,00 (total +209,93, echte
Steuerlast +28,93). Verzerrung rund 2.630 EUR - sie machte aus einem
profitablen Konto ein verlustreiches. Die Schaetzung der EINBEHALTENEN
Summe war fast exakt; falsch war allein die Annahme, sie bleibe weg.

trader.steuer_buchungen() liest die WHT-/Tax-Deals direkt
(position_id == 0, sauber von Trade-Deals getrennt), unter mt5_lock,
5 min gecacht. Fail-safe: ohne MT5 Rueckfall auf die Schaetzung, das
Feld wht_quelle sagt welche Zahl drinsteht.

Kurze Zeitraeume bleiben verzerrt, in BEIDE Richtungen - die Erstattung
kommt am Folgetag. Bei "today" ist wht sogar negativ, weil die
Erstattung von gestern heute einging. Ehrlich benannt, nicht kaschiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-12 20:44:09 +02:00
co-authored by Claude Opus 5
parent d8839ef7c5
commit 5819a9df21
3 changed files with 127 additions and 9 deletions
+28
View File
@@ -6209,6 +6209,34 @@ aber „20:11:42 CLOSE" sagte. ➜ **Epochs übergeben (lokal + Offset), beim Le
Moment des Closes, der Broker den Fill). Die Summe hebt sich fast auf (+1,66 € Moment des Closes, der Broker den Fill). Die Summe hebt sich fast auf (+1,66 €
über 19 Trades) → nur berichtet, **nicht** automatisch korrigiert. über 19 Trades) → nur berichtet, **nicht** automatisch korrigiert.
## ✅✅ NETTO-RECHNUNG AUS DEN ECHTEN STEUER-BUCHUNGEN (2026-08-12)
Der seit dem 05.08. dokumentierte Defekt ist behoben. `server._add_net` schaetzte
`net_pnl = total_pnl gross_win × 26,375 %` und unterstellte, die Quellensteuer
sei endgueltig verloren. Sie wird aber taeglich per `Tax settlement` erstattet.
| Zeitraum | total_pnl | wht (ECHT) | net_pnl | einbehalten / erstattet |
|---|---|---|---|---|
| today | 32,72 | 68,93 | **+36,21** | 21,03 / **+89,96** |
| week | 1.235,26 | 432,88 | 802,38 | 425,71 / +858,59 |
| **all** | **+209,93** | **+28,93** | **+181,00** | 2.754,56 / **+2.725,63** |
⚠⚠ **Vorher stand bei `all` 2.449,95 €** — eine Verzerrung von rund **2.630 €**,
die aus einem profitablen Konto ein verlustreiches machte.
✅ Die Schaetzung der EINBEHALTENEN Summe war fast exakt (2.620 gegen 2.755);
falsch war allein die Annahme, sie bleibe weg — real werden **99 %** erstattet.
**Umsetzung:** `trader.steuer_buchungen(von, bis)` liest die `WHT #…`- und
`Tax settlement`-Deals direkt (position_id == 0, damit sauber von den Trade-Deals
getrennt — kein Doppelzaehlen). Unter `mt5_lock`, **5 min gecacht** (die Abfrage
ueber lange Zeitraeume braucht Sekunden und gehoert nicht in jeden Aufruf).
**Fail-safe:** antwortet MT5 nicht, faellt es auf die alte Schaetzung zurueck —
das Feld **`wht_quelle`** sagt, welche Zahl drinsteht („echt" / „geschaetzt").
**KURZE ZEITRAEUME sind verzerrt, in BEIDE Richtungen:** die Erstattung kommt
erst am Folgetag. Bei „today" oben ist `wht` sogar **negativ** (68,93), weil die
Erstattung von GESTERN heute einging — das Netto liegt damit ueber dem Brutto.
Das ist korrekt gebucht, aber als Tageszahl irrefuehrend. Das Flag `wht_lag`
faengt bisher nur die eine Richtung (einbehalten, nichts erstattet).
⚠ Zeitfalle beachtet: Epochs mit Broker-Offset uebergeben, beim Lesen abziehen.
## ✅ STATISTIK-MODUL: ARITHMETIK KORREKT, NETTO-RECHNUNG WEITER DEFEKT (2026-08-12) ## ✅ 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 Gegen unabhängige SQL geprüft, alle drei Zeiträume: **Abweichung 0,00 in jeder
+59
View File
@@ -826,6 +826,65 @@ class TradeManager:
except Exception as e: except Exception as e:
log_hist.error(f"_log_external_close: {e}") log_hist.error(f"_log_external_close: {e}")
# ── ECHTE Steuer-Buchungen statt Schaetzung ──────────────────────────────
_STEUER_TTL_S = 300 # MT5-Abfrage gehoert nicht in jeden Aufruf
def steuer_buchungen(self, von_ts: int, bis_ts: int) -> dict | None:
"""Tatsaechlich einbehaltene und erstattete Quellensteuer im Zeitfenster.
⚠⚠ WARUM DAS DIE SCHAETZUNG ERSETZT (Befund 05.08., nachgemessen 12.08.):
`server._add_net` rechnete `net_pnl = total_pnl gross_win × 26,375 %` und
unterstellte damit, die WHT auf JEDEN Gewinn-Trade sei endgueltig verloren.
Sie wird aber **taeglich per `Tax settlement` erstattet**. Gemessen ueber
90 Tage: einbehalten 2.625,89 €, erstattet +2.608,83 € → verblieben
**17,06 €**. Die Schaetzung sagte 2.619,93 € und machte damit aus
**+153 € rund 2.450 €**.
✅ Die Schaetzung der EINBEHALTENEN Summe ist fast exakt (2.620 gegen
2.626) — falsch ist allein die Annahme, sie bleibe weg.
Rueckgabe `{wht, tax, netto, n}` in Kontowaehrung (wht negativ, tax
positiv, netto = Summe) oder `None`, wenn MT5 nicht antwortet.
⚠ Die Buchungen haben `position_id == 0` und sind damit von den Trade-Deals
sauber getrennt — kein Doppelzaehlen mit `total_pnl`.
⚠ ZEITFALLE: `history_deals_get` filtert nach BROKER-Wallclock. Deshalb
Epochs mit Offset uebergeben und beim Lesen wieder abziehen — mit
`datetime`-Objekten verschiebt sich das Fenster (real zweimal passiert).
⚠ Gecacht (`_STEUER_TTL_S`), weil die Abfrage ueber lange Zeitraeume
Sekunden braucht.
"""
schluessel = (int(von_ts), int(bis_ts))
cache = getattr(self, "_steuer_cache", None)
if cache and cache[0] == schluessel and time.time() - cache[1] < self._STEUER_TTL_S:
return cache[2]
off = self._broker_offset_s()
try:
with mt5_lock(timeout=8) as got:
if not got:
return None
deals = mt5.history_deals_get(int(von_ts) + off, int(bis_ts) + off + 300)
except Exception as e:
log_hist.warning(f"steuer_buchungen: {e}")
return None
if deals is None:
return None
wht = tax = 0.0
n = 0
for d in deals:
k = (getattr(d, "comment", "") or "")
if getattr(d, "position_id", 0):
continue # Trade-Deal, nicht Steuer
ts = int(getattr(d, "time", 0) or 0) - off
if not (von_ts <= ts <= bis_ts):
continue
if "WHT" in k:
wht += float(d.profit); n += 1
elif "Tax" in k:
tax += float(d.profit); n += 1
res = {"wht": round(wht, 2), "tax": round(tax, 2),
"netto": round(wht + tax, 2), "n": n}
self._steuer_cache = (schluessel, time.time(), res)
return res
def reconcile_open_trades(self, lookback_hours: int = 168): def reconcile_open_trades(self, lookback_hours: int = 168):
if not self.history: if not self.history:
return return
+39 -8
View File
@@ -20,6 +20,7 @@ Aktuell (Schritt 1): nur Read-only-Endpoints.
GET /api/snapshot → kompletter Live-Zustand (Markt/Position/Analyse/Agent) GET /api/snapshot → kompletter Live-Zustand (Markt/Position/Analyse/Agent)
""" """
from __future__ import annotations from __future__ import annotations
import time
import asyncio import asyncio
import contextlib import contextlib
import secrets import secrets
@@ -151,20 +152,49 @@ def logs(lines: int = 200):
return {"error": str(e), "lines": []} return {"error": str(e), "lines": []}
def _add_net(stats: dict, rate: float) -> dict: def _add_net(stats: dict, rate: float, trader=None) -> dict:
"""Ergänzt eine stats_overview-Periode um Netto-Werte nach Quellensteuer. """Ergänzt eine stats_overview-Periode um Netto-Werte nach Quellensteuer.
WHT wird nur auf die Gewinnseite (gross_win) erhoben, Verluste bleiben voll."""
⚠⚠ SEIT 2026-08-12 AUS DEN **ECHTEN** BUCHUNGEN, nicht mehr geschätzt.
Die alte Rechnung war `net_pnl = total_pnl gross_win × 26,375 %` und
unterstellte, die WHT sei endgültig verloren. Sie wird aber täglich per
`Tax settlement` erstattet — gemessen über 90 Tage zu **99 %**
(einbehalten 2.625,89 €, erstattet +2.608,83 €, verblieben 17,06 €).
Die Schätzung sagte 2.619,93 € und machte damit aus **+153 € rund
2.450 €**. ✅ Die Schätzung der EINBEHALTENEN Summe war fast exakt —
falsch war allein die Annahme, sie bleibe weg.
⚠ Fällt der Broker aus, wird auf die alte Schätzung zurückgefallen; das Feld
`wht_quelle` sagt, welche Zahl gerade drinsteht („echt" / „geschätzt").
⚠ KURZE ZEITRÄUME sind naturgemäss pessimistisch: die Erstattung kommt erst
am Folgetag. Für „today" fehlt sie also meist — deshalb `wht_lag`.
"""
if not stats or stats.get("error"): if not stats or stats.get("error"):
return stats return stats
gw = stats.get("gross_win", 0.0) or 0.0 gw = stats.get("gross_win", 0.0) or 0.0
gl = stats.get("gross_loss", 0.0) or 0.0 gl = stats.get("gross_loss", 0.0) or 0.0
tp = stats.get("total_pnl", 0.0) or 0.0
echt = None
if trader is not None:
try:
echt = trader.steuer_buchungen(int(stats.get("since") or 0),
int(stats.get("until") or time.time()))
except Exception:
echt = None
if echt and echt.get("n"):
wht = -float(echt["netto"]) # netto ist negativ → Last positiv
stats["wht_quelle"] = "echt"
stats["wht_brutto"] = echt["wht"]
stats["wht_erstattet"] = echt["tax"]
stats["wht_lag"] = bool(echt["tax"] == 0 and echt["wht"] < 0)
else:
wht = gw * rate wht = gw * rate
net_gw = gw - wht stats["wht_quelle"] = "geschätzt"
stats["wht_pct"] = round(rate * 100, 3) stats["wht_pct"] = round(rate * 100, 3)
stats["wht"] = wht stats["wht"] = round(wht, 2)
stats["net_pnl"] = (stats.get("total_pnl", 0.0) or 0.0) - wht stats["net_pnl"] = round(tp - wht, 2)
stats["net_profit_factor"] = (net_gw / gl) if gl > 0 else None stats["net_profit_factor"] = ((gw - wht) / gl) if gl > 0 else None
stats["net_avg_win"] = (stats.get("avg_win", 0.0) or 0.0) * (1 - rate) stats["net_avg_win"] = (stats.get("avg_win", 0.0) or 0.0) * (
1 - (wht / gw if gw > 0 else 0.0))
return stats return stats
@@ -187,7 +217,8 @@ def stats():
out: dict = {"wht_pct": round(rate * 100, 3)} out: dict = {"wht_pct": round(rate * 100, 3)}
for period in ("today", "week", "all"): for period in ("today", "week", "all"):
try: try:
out[period] = _add_net(eng.history.stats_overview(period), rate) out[period] = _add_net(eng.history.stats_overview(period), rate,
getattr(eng, "trader", None))
except Exception as e: except Exception as e:
out[period] = {"error": str(e)} out[period] = {"error": str(e)}
try: try: