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,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inter-Market-Kontext als Verdict-Kandidat (2026-07-19, User-Frage „weitere
|
||||
Indikatoren?"): die EINZIGE noch ungetestete Indikator-Klasse — Information von
|
||||
AUSSERHALB des WTI-Charts. Getestet werden zwei Hypothesen:
|
||||
|
||||
(A) Brent-Bestätigung: Brent-Trend (EMA12/50 M5) bestätigt das WTI-Signal →
|
||||
besserer Edge; Brent DAGEGEN → schlechter. (Brent-WTI laufen ~parallel,
|
||||
Divergenz = möglicher Fehlausbruch.)
|
||||
(B) DXY-Gegenwind: USDX-Trend gegen die Trade-Richtung (Öl in USD:
|
||||
Dollar rauf = Öl-Gegenwind für LONG, Dollar runter = Gegenwind für SHORT).
|
||||
|
||||
Methodik wie die 8 verworfenen Signal-Filter: ereignisbasierte EMA-Signale auf
|
||||
WTI M5 (frisches Kreuzen, Totband 0,15×ATR), sequentielle 1-Positions-Sim mit
|
||||
Live-Exit (SL 2,0×ATR + Trailing 1,5 + BE 1,3), Echtkosten = Bar-Spread/ATR.
|
||||
Jeder Trade wird nach dem Inter-Market-Zustand AM ENTRY gebucketed. 2 Halbjahre.
|
||||
|
||||
Verdict-Regel (vorab festgelegt, gegen Parameter-Fishing): ein Gate/Konfidenz-
|
||||
Einbau kommt NUR in Frage, wenn der „dagegen"-Bucket in BEIDEN Hälften klar
|
||||
schlechter ist als „dafür" UND das über beide Feature-Varianten (EMA-Stand und
|
||||
Momentum N=12/36) robust ist. Maßstab der 6×-Lektion: Signal-Filter tragen kaum.
|
||||
"""
|
||||
import sys
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
_MAXH = 288; _ATRMIN = 0.12; _COOL = 6; _DEAD = 0.15
|
||||
|
||||
|
||||
def _ema_series(C, p):
|
||||
k = 2.0 / (p + 1); e = C[0]; out = [e]
|
||||
for x in C[1:]:
|
||||
e = e + k * (x - e); out.append(e)
|
||||
return out
|
||||
|
||||
|
||||
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 _load(sym, n):
|
||||
mt5.symbol_select(sym, True)
|
||||
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
||||
if bars is None or len(bars) < 1000:
|
||||
print(f"FEHLER: keine Daten für {sym}"); sys.exit(1)
|
||||
point = mt5.symbol_info(sym).point
|
||||
T = [int(b["time"]) for b in bars]
|
||||
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
|
||||
C = [float(b["close"]) for b in bars]
|
||||
SP = [float(b["spread"])*point for b in bars]
|
||||
return T, H, L, C, SP
|
||||
|
||||
|
||||
def _dir_series(H, L, C):
|
||||
"""EMA12/50-Richtung mit Totband: +1/-1/0 je Bar."""
|
||||
e12 = _ema_series(C, 12); e50 = _ema_series(C, 50); A = _atr_series(H, L, C)
|
||||
out = []
|
||||
for i in range(len(C)):
|
||||
a = A[i] or 0
|
||||
if a <= 0: out.append(0); continue
|
||||
d = e12[i] - e50[i]
|
||||
out.append(1 if d > _DEAD*a else (-1 if d < -_DEAD*a else 0))
|
||||
return out, A
|
||||
|
||||
|
||||
def st(Rs):
|
||||
if not Rs: return None
|
||||
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
|
||||
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
|
||||
return dict(n=n, wr=100*w/n, oR=s/n, pf=(up/dn if dn > 0 else 9.99), sum=s)
|
||||
|
||||
|
||||
def line(lbl, s):
|
||||
if not s: return f" {lbl:<26} —"
|
||||
return (f" {lbl:<26} n={s['n']:>4} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
|
||||
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+6.0f}")
|
||||
|
||||
|
||||
def sim_trades(T, H, L, C, A, SP):
|
||||
"""Sequentielle Sim: frisches EMA-Signal → Trade mit Live-Exit. Liefert
|
||||
Liste (entry_index, dir, netto_R)."""
|
||||
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
|
||||
sig, _ = _dir_series(H, L, C)
|
||||
out = []; i = 60
|
||||
while i < len(C) - 2:
|
||||
if sig[i] == 0 or sig[i] == sig[i-1]:
|
||||
i += 1; continue
|
||||
d = sig[i]
|
||||
atr = max(A[i] or 0, _ATRMIN)
|
||||
entry = C[i]; eff = entry - d*2.0*atr; hw = entry
|
||||
end = min(i+_MAXH, len(C)-1); exit_px = C[end]; exit_j = end
|
||||
for j in range(i+1, end+1):
|
||||
hj, lj = H[j], L[j]
|
||||
if (lj <= eff) if d > 0 else (hj >= eff):
|
||||
exit_px = eff; exit_j = j; break
|
||||
hw = max(hw, hj) if d > 0 else min(hw, lj)
|
||||
prof = (C[j]-entry)*d
|
||||
if prof >= 0.3*atr:
|
||||
cand = hw - d*1.5*atr
|
||||
if prof >= 1.3*atr:
|
||||
cand = max(cand, entry) if d > 0 else min(cand, entry)
|
||||
eff = max(eff, cand) if d > 0 else min(eff, cand)
|
||||
out.append((i, d, (exit_px-entry)*d/atr - cost(i, atr)))
|
||||
i = exit_j + _COOL
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||
mt5.initialize()
|
||||
Tw, Hw, Lw, Cw, SPw = _load("SpotCrude", n+_MAXH+60)
|
||||
Tb, Hb, Lb, Cb, _ = _load("SpotBrent", n+_MAXH+60)
|
||||
Tx, Hx, Lx, Cx, _ = _load("USDX", n+_MAXH+60)
|
||||
mt5.shutdown()
|
||||
|
||||
dir_b, Ab = _dir_series(Hb, Lb, Cb)
|
||||
dir_x, Ax = _dir_series(Hx, Lx, Cx)
|
||||
idx_b = {t: i for i, t in enumerate(Tb)}
|
||||
idx_x = {t: i for i, t in enumerate(Tx)}
|
||||
Aw = _atr_series(Hw, Lw, Cw)
|
||||
|
||||
def look(idx, t):
|
||||
"""Index zum WTI-Zeitstempel (bis 2 Gitterschritte zurück — Feed-Lücken)."""
|
||||
for dt_ in (0, 300, 600):
|
||||
j = idx.get(t - dt_)
|
||||
if j is not None: return j
|
||||
return None
|
||||
|
||||
def mom_dir(C_, A_, j, N, thr=0.3, floor=None):
|
||||
if j is None or j < N: return None
|
||||
a = A_[j] or 0
|
||||
a = max(a, floor) if floor else (a if a > 0 else None)
|
||||
if not a: return None
|
||||
m = (C_[j] - C_[j-N]) / a
|
||||
return 1 if m > thr else (-1 if m < -thr else 0)
|
||||
|
||||
trades = sim_trades(Tw, Hw, Lw, Cw, Aw, SPw)
|
||||
print("="*88)
|
||||
print(f" Inter-Market-Kontext — WTI M5 ({len(trades)} Trades, seq. Sim, Live-Exit, Echtkosten)")
|
||||
print(f" Buckets nach Zustand AM ENTRY. Verdict nur bei Robustheit in BEIDEN Hälften.")
|
||||
print("="*88)
|
||||
|
||||
mid_t = Tw[len(Tw)//2]
|
||||
feats = [] # (half, dir, R, brent_ema, dxy_ema, brent_m12, brent_m36, dxy_m12, dxy_m36)
|
||||
miss_b = miss_x = 0
|
||||
for i, d, r in trades:
|
||||
t = Tw[i]
|
||||
jb = look(idx_b, t); jx = look(idx_x, t)
|
||||
if jb is None: miss_b += 1
|
||||
if jx is None: miss_x += 1
|
||||
h = 1 if t < mid_t else 2
|
||||
feats.append((h, d, r,
|
||||
dir_b[jb] if jb is not None else None,
|
||||
dir_x[jx] if jx is not None else None,
|
||||
mom_dir(Cb, Ab, jb, 12, floor=0.12),
|
||||
mom_dir(Cb, Ab, jb, 36, floor=0.12),
|
||||
mom_dir(Cx, Ax, jx, 12),
|
||||
mom_dir(Cx, Ax, jx, 36)))
|
||||
if miss_b or miss_x:
|
||||
print(f" (ohne Inter-Market-Match: Brent {miss_b} · USDX {miss_x} — übersprungen)")
|
||||
|
||||
def bucket3(val, d):
|
||||
"""dafür / dagegen / neutral relativ zur Trade-Richtung."""
|
||||
if val is None: return None
|
||||
if val == 0: return "neutral"
|
||||
return "dafür" if val == d else "dagegen"
|
||||
|
||||
def bucket_dxy(val, d):
|
||||
"""DXY: Dollar MIT der Trade-Richtung = Gegenwind (Öl invers zum Dollar)."""
|
||||
if val is None: return None
|
||||
if val == 0: return "neutral"
|
||||
return "Gegenwind" if val == d else "Rückenwind"
|
||||
|
||||
sections = [
|
||||
("(A) Brent-Trend (EMA12/50)", 3, bucket3),
|
||||
("(A2) Brent-Momentum 1h (N=12)", 5, bucket3),
|
||||
("(A3) Brent-Momentum 3h (N=36)", 6, bucket3),
|
||||
("(B) DXY-Trend (EMA12/50)", 4, bucket_dxy),
|
||||
("(B2) DXY-Momentum 1h (N=12)", 7, bucket_dxy),
|
||||
("(B3) DXY-Momentum 3h (N=36)", 8, bucket_dxy),
|
||||
]
|
||||
order = ("dafür", "dagegen", "neutral", "Rückenwind", "Gegenwind")
|
||||
for title, col, bfn in sections:
|
||||
print(f"\n {title}:")
|
||||
for h in (1, 2):
|
||||
print(f" H{h}:")
|
||||
groups = {}
|
||||
for row in feats:
|
||||
if row[0] != h: continue
|
||||
b = bfn(row[col], row[1])
|
||||
if b is None: continue
|
||||
groups.setdefault(b, []).append(row[2])
|
||||
for name in order:
|
||||
if name in groups:
|
||||
print(line(name, st(groups[name])))
|
||||
|
||||
print("\n Verdict-Regel: Einbau NUR wenn 'dagegen'/'Gegenwind' in BEIDEN Hälften")
|
||||
print(" klar schlechter als 'dafür'/'Rückenwind' UND über die Varianten robust.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user