bots/mcp_server.py aktualisiert
All checks were successful
MT5 Helm GitOps Deploy / build-and-deploy (push) Has been skipped

This commit is contained in:
bast 2026-11-06 09:21:21 +00:00
parent c5c0d42eac
commit c4f0ddce08

View file

@ -257,7 +257,8 @@ class LiveBridge(MT5Bridge):
self._rpyc_conn._config['sync_request_timeout'] = RPYC_TIMEOUT self._rpyc_conn._config['sync_request_timeout'] = RPYC_TIMEOUT
self._rpyc_conn.execute("import MetaTrader5 as mt5") self._rpyc_conn.execute("import MetaTrader5 as mt5")
self._rpyc_conn.execute("from datetime import datetime, timedelta") self._rpyc_conn.execute("from datetime import datetime, timedelta")
self._rpyc_conn.execute("mt5.initialize()") p = os.getenv("MT5_PATH")
self._rpyc_conn.execute(f"mt5.initialize(path={p!r})" if p else "mt5.initialize()")
log.info("Direct rpyc connection established for tick data") log.info("Direct rpyc connection established for tick data")
except Exception as e: except Exception as e:
log.error(f"rpyc connection failed: {e}") log.error(f"rpyc connection failed: {e}")
@ -272,19 +273,38 @@ class LiveBridge(MT5Bridge):
# initialize() mit Login startet/verbindet das Terminal unter Wine und # initialize() mit Login startet/verbindet das Terminal unter Wine und
# dauert oft >30s (RPyC-Default) -> sonst "result expired" # dauert oft >30s (RPyC-Default) -> sonst "result expired"
self.mt5._MetaTrader5__conn._config['sync_request_timeout'] = RPYC_TIMEOUT self.mt5._MetaTrader5__conn._config['sync_request_timeout'] = RPYC_TIMEOUT
# Broker-Login aus Umgebungsvariablen (bots.env) – leer = Terminal-Login verwenden # Schritt 1: Bridge – nur ans Terminal anhängen, noch KEIN Login.
kwargs = {} # Pfad nötig bei Broker-Terminals (sonst IPC timeout -10005).
if os.getenv("MT5_LOGIN"): init_kwargs = {"timeout": int(os.getenv("MT5_INIT_TIMEOUT_MS", "90000"))}
kwargs = { if os.getenv("MT5_PATH"):
"login": int(os.getenv("MT5_LOGIN")), init_kwargs["path"] = os.getenv("MT5_PATH")
"password": os.getenv("MT5_PASSWORD", ""), log.info(f"Connecting to terminal {init_kwargs.get('path', '(auto-detect)')}")
"server": os.getenv("MT5_SERVER", ""), if not self.mt5.initialize(**init_kwargs):
}
log.info(f"Logging in to MT5 account {kwargs['login']} on {kwargs['server']}")
if not self.mt5.initialize(**kwargs):
error = self.mt5.last_error() error = self.mt5.last_error()
log.error(f"MT5 initialize failed: {error}") log.error(f"MT5 initialize failed: {error}")
return False return False
log.info("Bridge OK – terminal attached")
# Schritt 2: Login nur, wenn nicht schon das richtige Konto angemeldet ist
want = os.getenv("MT5_LOGIN")
current = self.mt5.account_info()
current_login = current.login if current is not None else None
if want and current_login != int(want):
log.info(f"Account in terminal: {current_login} – logging in to {want} "
f"on {os.getenv('MT5_SERVER', '')}")
ok = self.mt5.login(int(want),
password=os.getenv("MT5_PASSWORD", ""),
server=os.getenv("MT5_SERVER", ""),
timeout=int(os.getenv("MT5_LOGIN_TIMEOUT_MS", "60000")))
if not ok:
log.error(f"MT5 login failed: {self.mt5.last_error()}")
return False
elif current_login is None:
log.error("Terminal has no logged-in account and MT5_LOGIN is not set")
return False
else:
log.info(f"Account {current_login} already logged in – no login needed")
info = self.mt5.account_info() info = self.mt5.account_info()
log.info(f"LiveBridge connected: login={info.login}, server={info.server}") log.info(f"LiveBridge connected: login={info.login}, server={info.server}")
self.connected = True self.connected = True
@ -297,6 +317,22 @@ class LiveBridge(MT5Bridge):
log.error(f"LiveBridge connection failed: {e}") log.error(f"LiveBridge connection failed: {e}")
return False return False
async def ensure(self):
"""Prüft vor jedem Aufruf, ob die IPC-Verbindung zum Terminal noch steht,
und verbindet sonst neu (z.B. nach Terminal-Neustart im Web-UI oder wenn
ein anderer Client im selben Wine-Python mt5.initialize/shutdown aufruft)."""
try:
if self.mt5 is not None and self.mt5.terminal_info() is not None:
return
if self.mt5 is not None:
log.warning(f"IPC connection lost: {self.mt5.last_error()} – reconnecting")
except Exception as e:
log.warning(f"Bridge check failed ({e}) – reconnecting")
self.connected = False
self._rpyc_conn = None
if not await self.connect():
raise HTTPException(503, "MT5 not connected (reconnect failed)")
async def disconnect(self): async def disconnect(self):
if self._rpyc_conn: if self._rpyc_conn:
try: try:
@ -312,8 +348,7 @@ class LiveBridge(MT5Bridge):
self.connected = False self.connected = False
async def account_info(self) -> Dict[str, Any]: async def account_info(self) -> Dict[str, Any]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
info = self.mt5.account_info() info = self.mt5.account_info()
if info is None: if info is None:
raise HTTPException(500, f"account_info failed: {self.mt5.last_error()}") raise HTTPException(500, f"account_info failed: {self.mt5.last_error()}")
@ -333,16 +368,14 @@ class LiveBridge(MT5Bridge):
} }
async def symbols(self) -> List[str]: async def symbols(self) -> List[str]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
syms = self.mt5.symbols_get() syms = self.mt5.symbols_get()
if syms is None: if syms is None:
return [] return []
return [s.name for s in syms if s.visible] return [s.name for s in syms if s.visible]
async def symbol_info(self, symbol: str) -> Dict[str, Any]: async def symbol_info(self, symbol: str) -> Dict[str, Any]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
info = self.mt5.symbol_info(symbol) info = self.mt5.symbol_info(symbol)
if info is None: if info is None:
raise HTTPException(404, f"Symbol {symbol} not found") raise HTTPException(404, f"Symbol {symbol} not found")
@ -361,8 +394,7 @@ class LiveBridge(MT5Bridge):
} }
async def ohlc(self, symbol: str, timeframe: str, bars: int, start_pos: int = 0) -> List[Dict]: async def ohlc(self, symbol: str, timeframe: str, bars: int, start_pos: int = 0) -> List[Dict]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
import pandas as pd import pandas as pd
# Check if seconds timeframe # Check if seconds timeframe
@ -407,8 +439,7 @@ class LiveBridge(MT5Bridge):
from_ts: Unix timestamp Start from_ts: Unix timestamp Start
to_ts: Unix timestamp Ende to_ts: Unix timestamp Ende
""" """
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
import pandas as pd import pandas as pd
from datetime import datetime, timezone from datetime import datetime, timezone
@ -543,8 +574,7 @@ class LiveBridge(MT5Bridge):
sl: float = 0, tp: float = 0, sl: float = 0, tp: float = 0,
comment: str = "") -> Dict[str, Any]: comment: str = "") -> Dict[str, Any]:
"""Open a market order.""" """Open a market order."""
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
# Get symbol info for filling # Get symbol info for filling
info = self.mt5.symbol_info(symbol) info = self.mt5.symbol_info(symbol)
@ -588,8 +618,7 @@ class LiveBridge(MT5Bridge):
async def order_modify(self, ticket: int, sl: float = None, async def order_modify(self, ticket: int, sl: float = None,
tp: float = None) -> Dict[str, Any]: tp: float = None) -> Dict[str, Any]:
"""Modify SL/TP of an existing position.""" """Modify SL/TP of an existing position."""
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
# Get current position # Get current position
positions = self.mt5.positions_get(ticket=ticket) positions = self.mt5.positions_get(ticket=ticket)
@ -619,8 +648,7 @@ class LiveBridge(MT5Bridge):
async def order_close(self, ticket: int) -> Dict[str, Any]: async def order_close(self, ticket: int) -> Dict[str, Any]:
"""Close an existing position.""" """Close an existing position."""
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
positions = self.mt5.positions_get(ticket=ticket) positions = self.mt5.positions_get(ticket=ticket)
if positions is None or len(positions) == 0: if positions is None or len(positions) == 0:
@ -659,8 +687,7 @@ class LiveBridge(MT5Bridge):
async def tick(self, symbol: str) -> Dict[str, Any]: async def tick(self, symbol: str) -> Dict[str, Any]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
tick = self.mt5.symbol_info_tick(symbol) tick = self.mt5.symbol_info_tick(symbol)
if tick is None: if tick is None:
raise HTTPException(404, f"No tick for {symbol}") raise HTTPException(404, f"No tick for {symbol}")
@ -674,8 +701,7 @@ class LiveBridge(MT5Bridge):
} }
async def positions(self) -> List[Dict]: async def positions(self) -> List[Dict]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
pos = self.mt5.positions_get() pos = self.mt5.positions_get()
if pos is None: if pos is None:
return [] return []
@ -699,8 +725,7 @@ class LiveBridge(MT5Bridge):
return result return result
async def orders(self) -> List[Dict]: async def orders(self) -> List[Dict]:
if not self.mt5: await self.ensure()
raise HTTPException(503, "MT5 not connected")
ords = self.mt5.orders_get() ords = self.mt5.orders_get()
if ords is None: if ords is None:
return [] return []
@ -794,6 +819,11 @@ async def root():
@app.get("/health") @app.get("/health")
async def health(): async def health():
if bridge.mode == "live":
try:
await bridge.ensure()
except HTTPException:
pass
return { return {
"status": "ok" if bridge.connected else "degraded", "status": "ok" if bridge.connected else "degraded",
"mode": bridge.mode, "mode": bridge.mode,