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:
co-authored by
Claude Opus 5
parent
b376619663
commit
3ec4d53022
@@ -784,6 +784,36 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
BOTS sind gemessen richtig; die eigentliche Asymmetrie liegt im diskretionären
|
BOTS sind gemessen richtig; die eigentliche Asymmetrie liegt im diskretionären
|
||||||
Früh-Kappen — genau die dokumentierte Kern-Leckage (B0-Checkliste: „Gewinner läuft
|
Früh-Kappen — genau die dokumentierte Kern-Leckage (B0-Checkliste: „Gewinner läuft
|
||||||
das Trailing"). Ein Code-Gate am Bot-Exit adressiert das falsche Ende.
|
das Trailing"). Ein Code-Gate am Bot-Exit adressiert das falsche Ende.
|
||||||
|
⚠⚠ **KONTRAFAKTISCH NACHGERECHNET (`analyze_manual_close.py`, 2026-08-04) — DIE
|
||||||
|
„FRÜH-KAPPEN"-HYPOTHESE IST WIDERLEGT.** Für jeden real geschlossenen Trade wurde
|
||||||
|
ab dem echten Einstieg der kanonische Exit simuliert (EUR-Faktor 87,6 €/$/Lot,
|
||||||
|
robust aus 251 Trades geschätzt — `trader.pnl` enthält Swap, eine direkte
|
||||||
|
Rückrechnung wäre verfälscht):
|
||||||
|
| Schließgrund | n | IST | Bot-Exit | Δ |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `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** |
|
||||||
|
**Das diskretionäre Früh-Kappen kostet NICHTS — es bringt +467 € MEHR** als der
|
||||||
|
kanonische Exit gebracht hätte; in **62 %** der Fälle war der manuelle Close
|
||||||
|
besser. Dasselbe gilt für den S/R-Auto-Close (−530). Beide realisieren Gewinne
|
||||||
|
besser, als ein reines Trailing es täte.
|
||||||
|
✅ **Die Leckage liegt auf der VERLUSTSEITE:** die 69 per Broker-SL geschlossenen
|
||||||
|
Trades hätten mit dem kanonischen Exit **1.229 € weniger** verloren (−697 statt
|
||||||
|
−1926), die 98 Notfall-Closes 515 € weniger. Über ALLE Trades summiert wäre der
|
||||||
|
kanonische Exit rund **+700 €** besser gewesen — praktisch vollständig aus der
|
||||||
|
Verlustseite.
|
||||||
|
⚠ **Confounder, der das Ergebnis begrenzt:** `simulate()` setzt den Initial-SL auf
|
||||||
|
**2,0×ATR_M5**, live kommt er aus **ATR_M15** (`_calc_sl_tp`) und ist damit oft
|
||||||
|
deutlich weiter — unter Margin-Sizing schlägt das voll auf den EUR-Verlust durch.
|
||||||
|
Der +1.229-Wert misst also zu einem erheblichen Teil **SL-Breite**, nicht
|
||||||
|
Exit-Logik. `backtest_sl_tf_mismatch.py` hat die Umstellung auf M5-ATR bereits
|
||||||
|
geprüft und wegen H2 verworfen — dieser Befund gibt Anlass, das mit dem
|
||||||
|
kanonischen Exit ZU WIEDERHOLEN.
|
||||||
|
⚠ Zweite Grenze: der Vergleich gilt **pro Trade**, nicht als Kontostand-
|
||||||
|
Hochrechnung — ein länger laufender Trade hätte den Positions-Slot belegt und
|
||||||
|
Folge-Trades verhindert.
|
||||||
- **Mindestgewinn für den S/R-Close = VERWORFEN (gemessen `backtest_srclose_prob.py`,
|
- **Mindestgewinn für den S/R-Close = VERWORFEN (gemessen `backtest_srclose_prob.py`,
|
||||||
Mindestgewinn-Sektion):** „nur closen wenn ≥minR im Plus" verliert **monoton in
|
Mindestgewinn-Sektion):** „nur closen wenn ≥minR im Plus" verliert **monoton in
|
||||||
BEIDEN Hälften** (minR 0,2: −444/−213 · 0,5: −1425/−854 R vs minR=0). Die vielen
|
BEIDEN Hälften** (minR 0,2: −444/−213 · 0,5: −1425/−854 R vs minR=0). Die vielen
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user