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:
co-authored by
Claude Opus 5
parent
d8839ef7c5
commit
5819a9df21
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
wht = gw * rate
|
tp = stats.get("total_pnl", 0.0) or 0.0
|
||||||
net_gw = gw - wht
|
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
|
||||||
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user