#!/usr/bin/env python3 """Ausführungs-Analyse (Track B, Mess-Kalibrierung — KEINE Strategie-Änderung): A) Kosten-Profil: echter Spread je Berlin-Stunde (Bar-Feld `spread`, in Points) absolut und als ×ATR — ersetzt die pauschale 0,1×ATR-Kostenannahme. B) Slippage: SL-Closes, die SCHLECHTER als der Initial-SL ausgeführt wurden (sichere Untergrenze der echten Slippage; getrailte SLs sind nicht rekonstruierbar). C) MAE/MFE der Live-Trades: wie weit liefen echte Trades ins Minus (MAE) und ins Plus (MFE), normiert auf ATR≈|Entry−InitialSL|/2 — validiert SL-Band, Breakeven-Schwelle (1,3) und Trailing an LIVE-Daten statt Simulation. Aufruf: python analyze_execution.py [m5_bars] [m1_bars] """ import sys, sqlite3, datetime as dt from zoneinfo import ZoneInfo from collections import defaultdict import MetaTrader5 as mt5 _BROKER_OFF = 3 * 3600 _BERLIN = ZoneInfo("Europe/Berlin") DB = "oil_widget_history.db" def bhour(broker_ts): return dt.datetime.fromtimestamp(int(broker_ts) - _BROKER_OFF, tz=dt.timezone.utc).astimezone(_BERLIN).hour 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]))) out = [] for i in range(len(C)): w = t[max(1, i-p+1):i+1] out.append(sum(w)/len(w) if w else None) return out def main(): n5 = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 n1 = int(sys.argv[2]) if len(sys.argv) > 2 else 100000 mt5.initialize() sym = None for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): if mt5.symbol_info(c): sym = c; break si = mt5.symbol_info(sym) point = si.point bars5 = None for req in (n5, 80000, 60000, 40000): bars5 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, req) if bars5 is not None and len(bars5) > 2000: break bars1 = None for req in (n1, 60000, 30000, 15000): bars1 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M1, 0, req) if bars1 is not None and len(bars1) > 1000: break mt5.shutdown() # ── A) Kosten-Profil: Spread je Berlin-Stunde ──────────────────────────── print("=" * 84) print(f" A) KOSTEN-PROFIL — {sym}, {len(bars5)} M5-Bars, Spread aus Bar-Feld (Points×{point})") print("=" * 84) H = [float(b["high"]) for b in bars5]; L = [float(b["low"]) for b in bars5] C = [float(b["close"]) for b in bars5] AT = _atr_series(H, L, C) byh = defaultdict(list) for i, b in enumerate(bars5): atr = AT[i] if not atr or atr <= 0: continue sp = float(b["spread"]) * point if sp <= 0: continue byh[bhour(b["time"])].append((sp, sp / atr)) print(f" {'Std':>3} {'Ø-Spread':>9} {'×ATR':>6} (Kostenannahme bisher pauschal 0,10×ATR)") tot = [] for h in range(24): v = byh.get(h) if not v: continue sp = sum(x[0] for x in v)/len(v); rel = sum(x[1] for x in v)/len(v) tot += v mark = " ⚠ teuer" if rel > 0.15 else (" günstig" if rel < 0.07 else "") print(f" {h:>3} {sp:>9.4f} {rel:>6.3f}{mark}") if tot: print(f" ALLE Ø-Spread {sum(x[0] for x in tot)/len(tot):.4f} · {sum(x[1] for x in tot)/len(tot):.3f}×ATR") # ── Trades laden (B + C) ───────────────────────────────────────────────── con = sqlite3.connect(DB); con.row_factory = sqlite3.Row rows = [dict(r) for r in con.execute( "SELECT * FROM trades WHERE exit_time IS NOT NULL AND entry_price IS NOT NULL " "ORDER BY entry_time")] # ── B) Slippage bei SL-Closes ──────────────────────────────────────────── print("\n" + "=" * 84) print(" B) SLIPPAGE — SL-Closes schlechter als der Initial-SL (sichere Untergrenze)") print("=" * 84) slips = [] n_sl = 0 for r in rows: if r["closed_by"] != "sl" or not r["sl_at_entry"]: continue n_sl += 1 d = 1 if r["direction"] in ("BUY", "LONG", "buy") else -1 gap = (r["sl_at_entry"] - r["exit_price"]) * d # >0 = schlechter als Initial-SL if gap > 0: slips.append((gap, r)) print(f" SL-Closes gesamt: {n_sl} · davon SCHLECHTER als Initial-SL: {len(slips)}") if slips: gaps = [g for g, _ in slips] gaps.sort() print(f" Slippage: Ø {sum(gaps)/len(gaps):.3f} · Median {gaps[len(gaps)//2]:.3f} " f"· Max {max(gaps):.3f} (Preis-Punkte)") worst = sorted(slips, key=lambda x: -x[0])[:5] for g, r in worst: t = dt.datetime.fromtimestamp(r["entry_time"]).strftime("%d.%m %H:%M") print(f" {t} {r['direction']:<4} SL {r['sl_at_entry']:.3f} → Exit " f"{r['exit_price']:.3f} Slippage {g:.3f} (netto {((r['pnl'] or 0)+(r['commission'] or 0)):+.2f})") # ── C) MAE/MFE der Live-Trades (M1-Fenster) ───────────────────────────── print("\n" + "=" * 84) print(f" C) MAE/MFE live — M1-Abdeckung {len(bars1)} Bars " f"(~{len(bars1)/60/24*1.0:.0f} Handelstage), ATR≈|Entry−InitSL|/2") print("=" * 84) T1 = [int(b["time"]) for b in bars1] H1 = [float(b["high"]) for b in bars1]; L1 = [float(b["low"]) for b in bars1] t0 = T1[0] import bisect cov = 0 mae_w, mae_l, mfe_w, mfe_l = [], [], [], [] for r in rows: eb = int(r["entry_time"]) + _BROKER_OFF # lokale Epoch → Broker-Epoch xb = int(r["exit_time"]) + _BROKER_OFF if eb < t0 or not r["sl_at_entry"]: continue i0 = bisect.bisect_left(T1, eb); i1 = bisect.bisect_right(T1, xb) if i1 - i0 < 1: continue d = 1 if r["direction"] in ("BUY", "LONG", "buy") else -1 entry = float(r["entry_price"]) atr_est = abs(entry - float(r["sl_at_entry"])) / 2.0 if atr_est <= 0: continue seg_h = H1[i0:i1]; seg_l = L1[i0:i1] # adverse/favorable Exkursion je Richtung if d > 0: mae = max(0.0, entry - min(seg_l)); mfe = max(0.0, max(seg_h) - entry) else: mae = max(0.0, max(seg_h) - entry); mfe = max(0.0, entry - min(seg_l)) net = (r["pnl"] or 0) + (r["commission"] or 0) cov += 1 (mae_w if net > 0 else mae_l).append(mae / atr_est) (mfe_w if net > 0 else mfe_l).append(mfe / atr_est) def st(v): if not v: return "n=0" v = sorted(v) return (f"n={len(v):>3} Ø={sum(v)/len(v):.2f} Median={v[len(v)//2]:.2f} " f"P90={v[int(len(v)*0.9)]:.2f}") print(f" Abgedeckte Trades: {cov}") print(f" MAE Gewinner {st(mae_w)} (wie tief liefen GEWINNER ins Minus)") print(f" MAE Verlierer {st(mae_l)}") print(f" MFE Gewinner {st(mfe_w)}") print(f" MFE Verlierer {st(mfe_l)} (wie viel Plus gaben VERLIERER wieder her)") if mfe_l: gave = sum(1 for x in mfe_l if x >= 1.3) / len(mfe_l) print(f" Verlierer, die ≥1,3×ATR im Plus waren (Breakeven hätte greifen müssen): {100*gave:.0f}%") if mae_w: deep = sum(1 for x in mae_w if x >= 1.8) / len(mae_w) print(f" Gewinner, die ≥1,8×ATR im Minus waren (SL-Band-Nähe überlebt): {100*deep:.0f}%") if __name__ == "__main__": main()