Initial commit: multi-symbol bot with backtest engine and RSI trend strategy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user