Files
AH-Oil-Trader/analyze_execution.py
T
Axel HocksandClaude Opus 4.8 75d28827e8 Initial commit: Oil Trading Bot (MT5, WTI)
Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:29:23 +02:00

162 lines
7.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
"""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≈|EntryInitialSL|/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≈|EntryInitSL|/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()