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
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Sucht ungefüllte Kurslücken (Gaps) im WTI-Tageschart 2026.
Gap = Vakuum zwischen Vortag und Folgetag:
Gap UP : Low(heute) > High(gestern) → Lücke [High_gestern … Low_heute]
Gap DOWN : High(heute) < Low(gestern) → Lücke [High_heute … Low_gestern]
„Gefüllt" = ein SPÄTERER Bar handelt wieder in/durch das Vakuum (Kurs kehrt zur
Gap-Kante zurück). Ausgegeben werden die NOCH OFFENEN (ungefüllten) Gaps + das
Kursniveau, das sie schließen würde, und der Abstand zum aktuellen Kurs.
"""
import datetime as dt
import MetaTrader5 as mt5
OFF = 3 * 3600 # Broker UTC+3 → UTC (nur für die Datumsanzeige grob)
def main():
if not mt5.initialize():
print("MT5 init fehlgeschlagen:", mt5.last_error()); return
sym = None
for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
if mt5.symbol_info(c): sym = c; break
# genug D1-Bars holen (ganzes Jahr + Puffer)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_D1, 0, 400)
tick = mt5.symbol_info_tick(sym)
price = (tick.bid + tick.ask) / 2 if tick else None
mt5.shutdown()
if bars is None:
print("Keine Bars"); return
rows = []
for b in bars:
d = dt.datetime.utcfromtimestamp(int(b["time"]) - OFF).date()
rows.append({"date": d, "o": float(b["open"]), "h": float(b["high"]),
"l": float(b["low"]), "c": float(b["close"])})
rows = [r for r in rows if r["date"].year == 2026]
if len(rows) < 2:
print("Zu wenig 2026-Daten"); return
gaps = []
for i in range(1, len(rows)):
p, cur = rows[i - 1], rows[i]
if cur["l"] > p["h"]: # Gap UP
gaps.append({"i": i, "date": cur["date"], "dir": "UP",
"lo": p["h"], "hi": cur["l"], "ref_close": p["c"]})
elif cur["h"] < p["l"]: # Gap DOWN
gaps.append({"i": i, "date": cur["date"], "dir": "DOWN",
"lo": cur["h"], "hi": p["l"], "ref_close": p["c"]})
# Fill-Status: handelt ein späterer Bar in das Vakuum zurück?
for g in gaps:
later = rows[g["i"] + 1:]
if g["dir"] == "UP":
# gefüllt, sobald ein späterer Low <= Gap-Unterkante (zurück in die Lücke)
mn = min((r["l"] for r in later), default=price or g["hi"])
g["filled"] = mn <= g["lo"]
g["fill_target"] = g["lo"] # Niveau, das die Lücke schließt
g["partial"] = (not g["filled"]) and mn < g["hi"]
else:
mx = max((r["h"] for r in later), default=price or g["lo"])
g["filled"] = mx >= g["hi"]
g["fill_target"] = g["hi"]
g["partial"] = (not g["filled"]) and mx > g["lo"]
g["size"] = g["hi"] - g["lo"]
openg = [g for g in gaps if not g["filled"]]
print("=" * 78)
print(f" WTI ({sym}) 2026 — Gap-Analyse (D1) aktueller Kurs ≈ {price:.2f}"
if price else f" WTI ({sym}) 2026 — Gap-Analyse (D1)")
print(f" {len(rows)} Handelstage · {len(gaps)} Gaps gesamt · "
f"{len(openg)} NOCH OFFEN")
print("=" * 78)
if not openg:
print(" Keine offenen Gaps — alle 2026er Lücken wurden gefüllt.")
return
print(f" {'Datum':<12}{'Richtg':<7}{'Lücke vonbis':<18}{'Größe':>7}"
f"{'Schließt bei':>14}{'Abstand':>10} Status")
for g in sorted(openg, key=lambda x: x["date"]):
dist = (g["fill_target"] - price) if price else 0.0
tag = "teilw. angelaufen" if g.get("partial") else "unberührt"
zone = f"{g['lo']:.2f}{g['hi']:.2f}"
print(f" {g['date'].isoformat():<12}{g['dir']:<7}{zone:<18}"
f"{g['size']:>7.2f}{g['fill_target']:>14.2f}{dist:>+10.2f} {tag}")
print("\n „Schließt bei\" = Kursniveau, das die Lücke füllt · "
"Abstand = von dort zum aktuellen Kurs (+ = darüber, = darunter)")
if __name__ == "__main__":
main()