Files
AH-Oil-Trader/analyze_manual_close.py
T
Axel HocksandClaude Opus 5 3ec4d53022 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>
2026-08-04 13:25:01 +02:00

124 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()