diff --git a/CLAUDE.md b/CLAUDE.md index c4ddb35..7d54eed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -784,6 +784,36 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: BOTS sind gemessen richtig; die eigentliche Asymmetrie liegt im diskretionären 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. + ⚠⚠ **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-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 diff --git a/analyze_manual_close.py b/analyze_manual_close.py new file mode 100644 index 0000000..05d1010 --- /dev/null +++ b/analyze_manual_close.py @@ -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()