bots/mcp_server.py aktualisiert
All checks were successful
MT5 Helm GitOps Deploy / build-and-deploy (push) Has been skipped
All checks were successful
MT5 Helm GitOps Deploy / build-and-deploy (push) Has been skipped
This commit is contained in:
parent
c5c0d42eac
commit
c4f0ddce08
1 changed files with 63 additions and 33 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue