Files
AH-Oil-Trader/core/mt5_utils.py
T
Axel HocksandClaude Opus 4.8 75d28827e8 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>
2026-07-24 08:29:23 +02:00

162 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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