Files
TradingBot-NewBot/bot/mt5_client.py
T

131 lines
3.5 KiB
Python

"""
MT5 broker client. Thin async wrapper around the synchronous MetaTrader5 API.
All blocking calls run in the default executor so the event loop stays free.
"""
import asyncio
import logging
from typing import Optional
import pandas as pd
from . import config
logger = logging.getLogger(__name__)
try:
import MetaTrader5 as mt5
_MT5_AVAILABLE = True
except ImportError:
_MT5_AVAILABLE = False
logger.warning("MetaTrader5 package not installed — MT5 client unavailable")
ORDER_TYPE_BUY = 0
ORDER_TYPE_SELL = 1
_TF_MAP = {
"M1": 1, "M5": 5, "M15": 15, "M30": 30,
"H1": 16385, "H4": 16388, "D1": 16408,
}
def _run(fn):
return asyncio.get_event_loop().run_in_executor(None, fn)
def connect() -> bool:
if not _MT5_AVAILABLE:
logger.error("MetaTrader5 not installed")
return False
if not mt5.initialize(
login=config.MT5_LOGIN,
password=config.MT5_PASSWORD,
server=config.MT5_SERVER,
):
logger.error("MT5 init failed: %s", mt5.last_error())
return False
info = mt5.terminal_info()
logger.info("MT5 connected: %s", info.name if info else "unknown")
return True
def disconnect():
if _MT5_AVAILABLE:
mt5.shutdown()
async def get_candles(symbol: str, timeframe: str, count: int) -> Optional[pd.DataFrame]:
if not _MT5_AVAILABLE:
return None
tf = _TF_MAP[timeframe]
rates = await _run(lambda: mt5.copy_rates_from_pos(symbol, tf, 0, count))
if rates is None or len(rates) == 0:
return None
df = pd.DataFrame(rates)
df["time"] = pd.to_datetime(df["time"], unit="s")
df = df.rename(columns={"tick_volume": "tick_volume"})
return df.set_index("time")
async def get_tick(symbol: str) -> Optional[dict]:
if not _MT5_AVAILABLE:
return None
tick = await _run(lambda: mt5.symbol_info_tick(symbol))
return tick._asdict() if tick else None
async def get_account_info() -> Optional[dict]:
if not _MT5_AVAILABLE:
return None
info = await _run(mt5.account_info)
return info._asdict() if info else None
async def get_open_positions(magic: int = 234001) -> list[dict]:
if not _MT5_AVAILABLE:
return []
positions = await _run(lambda: mt5.positions_get())
if positions is None:
return []
return [p._asdict() for p in positions if p.magic == magic]
async def get_symbol_info(symbol: str) -> Optional[dict]:
if not _MT5_AVAILABLE:
return None
info = await _run(lambda: mt5.symbol_info(symbol))
return info._asdict() if info else None
async def place_order(
symbol: str,
order_type: int,
volume: float,
price: float,
sl: float,
tp: float,
comment: str = "",
magic: int = 234001,
) -> Optional[dict]:
if not _MT5_AVAILABLE:
return None
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": round(volume, 2),
"type": order_type,
"price": price,
"sl": sl,
"tp": tp,
"deviation": 20,
"magic": magic,
"comment": comment,
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_IOC,
}
result = await _run(lambda: mt5.order_send(request))
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
logger.error("Order failed %s: %s", symbol, result)
return None
return result._asdict()