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 @@
|
||||
"""
|
||||
core/mt5_utils.py — Thread-sicherer MT5-Zugriff + Trading-Hilfsfunktionen
|
||||
==========================================================================
|
||||
Globaler Lock für die MetaTrader5-Lib (nicht thread-safe) sowie
|
||||
alle kleinen Hilfsfunktionen die von TradeManager, TrailingManager
|
||||
und MT5Data gemeinsam genutzt werden.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
from core.config import (
|
||||
get_margin_buffer, SL_TF, SL_LOOKBACK, SL_WINDOW, SL_BUFFER_TICKS,
|
||||
)
|
||||
from core.logger import get_logger
|
||||
|
||||
log = get_logger("mt5")
|
||||
|
||||
# ── Globaler MT5-Lock ──────────────────────────
|
||||
# Die MT5 Python-Lib ist NICHT thread-safe. Alle MT5-Calls laufen
|
||||
# über diesen Lock. Mit Timeout, damit ein hängender order_send
|
||||
# (Server antwortet nicht) nicht alle anderen Threads blockiert.
|
||||
_mt5_lock = threading.Lock()
|
||||
MT5_LOCK_TIMEOUT_S = 5
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mt5_lock(timeout: float = MT5_LOCK_TIMEOUT_S):
|
||||
"""
|
||||
Context-Manager für den globalen MT5-Lock mit Timeout.
|
||||
|
||||
with mt5_lock() as got:
|
||||
if not got:
|
||||
return # Lock nicht erhalten → Tick überspringen
|
||||
... MT5-Calls ...
|
||||
"""
|
||||
acquired = _mt5_lock.acquire(timeout=timeout)
|
||||
if not acquired:
|
||||
try:
|
||||
yield False
|
||||
finally:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
yield True
|
||||
finally:
|
||||
_mt5_lock.release()
|
||||
|
||||
|
||||
# ── Tick / Symbol-Helpers ─────────────────────
|
||||
|
||||
def get_tick(sym: str):
|
||||
t = mt5.symbol_info_tick(sym)
|
||||
return t if (t and t.bid > 0 and t.ask > 0) else None
|
||||
|
||||
|
||||
def get_filling(sym: str) -> int:
|
||||
si = mt5.symbol_info(sym)
|
||||
if si is None:
|
||||
return mt5.ORDER_FILLING_IOC
|
||||
for m in (mt5.ORDER_FILLING_FOK, mt5.ORDER_FILLING_IOC,
|
||||
mt5.ORDER_FILLING_RETURN):
|
||||
if si.filling_mode & m:
|
||||
return m
|
||||
return mt5.ORDER_FILLING_RETURN
|
||||
|
||||
|
||||
def calc_lots(sym: str, price: float, otype: int) -> float:
|
||||
si, acc = mt5.symbol_info(sym), mt5.account_info()
|
||||
if not si or not acc:
|
||||
return 0.0
|
||||
mpl = mt5.order_calc_margin(otype, sym, 1.0, price)
|
||||
if not mpl or mpl <= 0:
|
||||
return 0.0
|
||||
step = si.volume_step
|
||||
lots = round(((acc.margin_free * get_margin_buffer() / mpl) // step) * step, 4)
|
||||
# Margin-Guard: reicht die freie Margin nicht mal fürs Mindestvolumen, 0.0
|
||||
# zurückgeben → Caller meldet sauber „Lot-Fehler" statt Broker-Reject mit
|
||||
# kryptischem retcode (früher: max(volume_min, …) erzwang unbezahlbare Größe).
|
||||
if lots < si.volume_min:
|
||||
return 0.0
|
||||
return min(lots, si.volume_max)
|
||||
|
||||
|
||||
def calc_lots_risk(sym: str, price: float, otype: int,
|
||||
sl_distance: float | None, risk_frac: float) -> float:
|
||||
"""Risiko-basierte Lot-Größe: Verlust beim Initial-SL ≈ risk_frac × Equity.
|
||||
|
||||
Ersetzt das All-in-auf-freie-Margin von `calc_lots`. Die freie Margin bleibt
|
||||
Obergrenze (Deckel), damit Mini-Konten nicht über die Margin hinaus ordern.
|
||||
Gibt 0.0 zurück, wenn Daten fehlen → Caller fällt auf `calc_lots` zurück.
|
||||
"""
|
||||
si, acc = mt5.symbol_info(sym), mt5.account_info()
|
||||
if not si or not acc or not sl_distance or sl_distance <= 0:
|
||||
return 0.0
|
||||
step = si.volume_step or 0.01
|
||||
tick_size = si.trade_tick_size or si.point
|
||||
tick_value = si.trade_tick_value
|
||||
if not tick_size or not tick_value:
|
||||
return 0.0
|
||||
val_per_price = tick_value / tick_size # € je 1.0 Preis je 1 Lot
|
||||
equity = acc.equity or acc.balance or 0.0
|
||||
if equity <= 0:
|
||||
return 0.0
|
||||
raw = (equity * risk_frac) / (sl_distance * val_per_price)
|
||||
lots = (raw // step) * step
|
||||
# Margin-Deckel: nie mehr als die freie Margin (× Buffer) zulässt
|
||||
mpl = mt5.order_calc_margin(otype, sym, 1.0, price)
|
||||
if mpl and mpl > 0:
|
||||
max_aff = ((acc.margin_free * get_margin_buffer() / mpl) // step) * step
|
||||
lots = min(lots, max_aff)
|
||||
lots = min(round(lots, 4), si.volume_max)
|
||||
# Ergibt die Risiko-Rechnung WENIGER als das Mindestlot, NICHT auf volume_min
|
||||
# aufrunden (das überschritte still das gewollte Risiko) → 0.0, Caller lehnt ab
|
||||
# (Fix 2026-07-19; vorher max(volume_min, …)).
|
||||
if lots < si.volume_min:
|
||||
return 0.0
|
||||
return lots
|
||||
|
||||
|
||||
def atr_value(sym: str, tf: int = SL_TF, period: int = 14) -> float | None:
|
||||
"""ATR (Wilder-vereinfacht: Mittel der True Ranges) auf `tf`."""
|
||||
r = mt5.copy_rates_from_pos(sym, tf, 0, period + 1)
|
||||
if r is None or len(r) < period + 1:
|
||||
return None
|
||||
trs = []
|
||||
for i in range(1, len(r)):
|
||||
h, l, pc = float(r[i]["high"]), float(r[i]["low"]), float(r[i - 1]["close"])
|
||||
trs.append(max(h - l, abs(h - pc), abs(l - pc)))
|
||||
return sum(trs) / len(trs) if trs else None
|
||||
|
||||
|
||||
def pivot_low(sym: str, max_above: float):
|
||||
r = mt5.copy_rates_from_pos(sym, SL_TF, 0, SL_LOOKBACK)
|
||||
if r is None or len(r) < 2 * SL_WINDOW + 1:
|
||||
return None
|
||||
lows = [float(x["low"]) for x in r]
|
||||
for i in range(len(lows) - SL_WINDOW - 1, SL_WINDOW - 1, -1):
|
||||
c = lows[i]
|
||||
if (c < max_above
|
||||
and c < min(lows[i - SL_WINDOW:i])
|
||||
and c < min(lows[i + 1:i + 1 + SL_WINDOW])):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def pivot_high(sym: str, min_below: float):
|
||||
r = mt5.copy_rates_from_pos(sym, SL_TF, 0, SL_LOOKBACK)
|
||||
if r is None or len(r) < 2 * SL_WINDOW + 1:
|
||||
return None
|
||||
highs = [float(x["high"]) for x in r]
|
||||
for i in range(len(highs) - SL_WINDOW - 1, SL_WINDOW - 1, -1):
|
||||
c = highs[i]
|
||||
if (c > min_below
|
||||
and c > max(highs[i - SL_WINDOW:i])
|
||||
and c > max(highs[i + 1:i + 1 + SL_WINDOW])):
|
||||
return c
|
||||
return None
|
||||
Reference in New Issue
Block a user