Frueh-Kappen kontrafaktisch geprueft — Hypothese widerlegt, Leckage liegt woanders

analyze_manual_close.py simuliert fuer jeden real geschlossenen Trade ab dem
echten Einstieg den kanonischen Exit und vergleicht gegen das tatsaechliche
Ergebnis. EUR-Faktor 87,6 EUR/$/Lot, robust aus 251 Trades geschaetzt (trader.pnl
enthaelt Swap, direkte Rueckrechnung waere verfaelscht).

  Schliessgrund        n     IST   Bot-Exit      Delta
  manual   Gewinn    143   +1428      +962       -467
  sr_close Gewinn    106   +1591     +1060       -530
  emergency Verlust   98   -1750     -1235       +515
  sl        Verlust   69   -1926      -697      +1229

Die Frueh-Kappen-Hypothese ist WIDERLEGT: das diskretionaere Schliessen kostet
nichts, es bringt +467 EUR MEHR als der kanonische Exit; in 62 % der Faelle war
der manuelle Close besser. Dasselbe gilt fuer den S/R-Auto-Close (-530). Beide
realisieren Gewinne besser als reines Trailing.

Die Leckage liegt auf der VERLUSTSEITE: die 69 per Broker-SL geschlossenen
Trades haetten mit dem kanonischen Exit 1.229 EUR weniger verloren, die 98
Notfall-Closes 515 EUR weniger. Ueber alle Trades waere der kanonische Exit rund
+700 EUR besser gewesen, praktisch vollstaendig aus der Verlustseite.

Confounder benannt: simulate() setzt den Initial-SL auf 2,0xATR_M5, live kommt er
aus ATR_M15 und ist oft deutlich weiter - unter Margin-Sizing schlaegt das voll
auf den EUR-Verlust durch. Der +1.229-Wert misst also erheblich SL-BREITE, nicht
Exit-Logik. backtest_sl_tf_mismatch.py hat die M5-Umstellung schon geprueft und
wegen H2 verworfen; dieser Befund gibt Anlass, das mit dem kanonischen Exit zu
wiederholen.

Zweite Grenze: Vergleich gilt pro Trade, nicht als Kontostand-Hochrechnung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-04 13:25:01 +02:00
co-authored by Claude Opus 5
parent b376619663
commit 3ec4d53022
2 changed files with 153 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Kosten des diskretionären Früh-Kappens — kontrafaktisch (2026-08-04).
Frage: Deine MANUELLEN Closes haben einen Median-Gewinn von +6,37 €, die
S/R-Auto-Closes des Bots +14,65 €. Ist das Früh-Kappen teuer — oder rettet es
Trades, die sonst gedreht wären?
Methode (wie `analyze_srclose_live.py`): Für jeden real geschlossenen Trade wird
ab dem ECHTEN Einstieg der **kanonische Live-Exit** (`core/exit_model.LIVE`)
über die M5-Bars simuliert. Verglichen wird das tatsächliche Ergebnis gegen das,
was der Bot-Exit erbracht hätte.
⚠ Grenzen, die das Ergebnis NICHT überschreiten darf:
· Kontrafaktisch heißt: der Trade hätte den Slot länger belegt. Folge-Trades,
die real stattfanden, wären dann evtl. nicht zustande gekommen. Der Vergleich
ist also PRO TRADE gültig, nicht als Kontostand-Hochrechnung.
· Der EUR-Faktor wird aus den echten Trades geschätzt (Median über Trades mit
klarem Move und kurzer Laufzeit, damit der Swap nicht verzerrt) — `trader.pnl`
enthält Swap, eine direkte Rückrechnung wäre verfälscht.
· Slippage ist nicht modelliert (real bis 0,75×ATR über den Stop hinaus gemessen).
Aufruf: python analyze_manual_close.py [tage]
"""
import bisect
import datetime as dt
import sqlite3
import statistics as st
import sys
import MetaTrader5 as mt5
from core.exit_model import LIVE, simulate
_BOFF = 3 * 3600
_ATRMIN = 0.06
def _atr_series(H, L, C, p=14):
t = [0.0]
for i in range(1, len(C)):
t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1])))
if i else None for i in range(len(C))]
def main():
tage = int(sys.argv[1]) if len(sys.argv) > 1 else 30
mt5.initialize()
bars = mt5.copy_rates_from_pos("SpotCrude", mt5.TIMEFRAME_M5, 0, 60000)
mt5.shutdown()
T = [int(b["time"]) - _BOFF for b in bars] # → lokale Epoch
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
C = [float(b["close"]) for b in bars]
A = _atr_series(H, L, C)
c = sqlite3.connect("oil_widget_history.db"); c.row_factory = sqlite3.Row
seit = (dt.datetime.now() - dt.timedelta(days=tage)).timestamp()
rows = [r for r in c.execute(
"SELECT * FROM trades WHERE exit_time>=? AND pnl IS NOT NULL "
"AND lots>0 ORDER BY entry_time", (seit,))]
# EUR je 1,00 $ und Lot robust schätzen: nur Trades mit klarem Move und
# kurzer Laufzeit (Swap fällt kaum ins Gewicht).
f = []
for r in rows:
mv = (r["exit_price"] - r["entry_price"]) * (1 if r["direction"] == "BUY" else -1)
dur = (r["exit_time"] - r["entry_time"]) / 60
if abs(mv) > 0.20 and dur < 120 and r["lots"]:
f.append(r["pnl"] / (mv * r["lots"]))
faktor = st.median(f) if f else 87.0
print(f"EUR je 1,00 $ und Lot (geschätzt aus {len(f)} Trades): {faktor:.1f}\n")
gruppen = {}
for r in rows:
i = bisect.bisect_left(T, r["entry_time"])
if i <= 0 or i >= len(T) - LIVE.max_hold - 2 or abs(T[i] - r["entry_time"]) > 900:
continue
atr = A[i]
if not atr or atr < _ATRMIN:
continue
d = 1 if r["direction"] == "BUY" else -1
R, _xj = simulate(r["entry_price"], d, atr, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
soll = R * atr * r["lots"] * faktor # was der Bot-Exit gebracht hätte
ist = r["pnl"]
key = (r["closed_by"] or "?", "Gewinn" if ist > 0 else "Verlust")
gruppen.setdefault(key, []).append((ist, soll, r))
print(f"{'Schließgrund':<16}{'Lage':<9}{'n':>5}{'IST Σ':>10}{'Bot-Exit Σ':>12}"
f"{'Δ':>10}{'Δ je Trade':>12}")
print("-" * 76)
for key in sorted(gruppen, key=lambda k: -len(gruppen[k])):
v = gruppen[key]
ist = sum(x[0] for x in v); soll = sum(x[1] for x in v)
print(f"{key[0]:<16}{key[1]:<9}{len(v):>5}{ist:>+10.0f}{soll:>+12.0f}"
f"{soll-ist:>+10.0f}{(soll-ist)/len(v):>+12.1f}")
man = [x for k, v in gruppen.items() if k[0] == "manual" for x in v]
if man:
besser = sum(1 for i_, s_, _ in man if s_ > i_)
print(f"\n MANUELLE Closes gesamt: n={len(man)} "
f"IST {sum(x[0] for x in man):+.0f}"
f"Bot-Exit {sum(x[1] for x in man):+.0f}"
f"Δ {sum(x[1]-x[0] for x in man):+.0f}")
print(f" In {besser} von {len(man)} Fällen ({100*besser/len(man):.0f} %) hätte "
f"der Bot-Exit mehr gebracht.")
gew = [x for x in man if x[0] > 0]
if gew:
print(f" Nur die manuellen GEWINN-Closes: n={len(gew)} "
f"Δ {sum(x[1]-x[0] for x in gew):+.0f}"
f"({sum(x[1]-x[0] for x in gew)/len(gew):+.1f} € je Trade)")
print("\n Die 8 teuersten Früh-Kappungen:")
for i_, s_, r in sorted(gew, key=lambda x: x[0] - x[1])[:8]:
print(f" {dt.datetime.fromtimestamp(r['exit_time']).strftime('%d.%m %H:%M')} "
f"IST {i_:+7.2f} Bot-Exit {s_:+7.2f} Δ {s_-i_:+7.2f} "
f"{r['direction']} {r['lots']}L")
print("\n ⚠ PRO TRADE gültig, nicht als Kontostand-Hochrechnung: ein länger")
print(" laufender Trade hätte den Positions-Slot belegt und Folge-Trades verhindert.")
print()
if __name__ == "__main__":
main()