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:
@@ -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≈|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()
|
||||
Reference in New Issue
Block a user