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>
This commit is contained in:
Axel Hocks
2026-07-24 08:29:23 +02:00
co-authored by Claude Opus 4.8
commit 75d28827e8
104 changed files with 21059 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
#!/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()