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>
124 lines
5.4 KiB
Python
124 lines
5.4 KiB
Python
#!/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()
|