""" HTTP bridge for MetaTrader 5 data. Run this on the Windows VM where MetaTrader 5 is installed and connected: py -m pip install MetaTrader5 set MT5_BRIDGE_TOKEN=change-me py mt5_bridge.py --host 0.0.0.0 --port 8765 Then configure TradeIA: MT5_BRIDGE_URL=http://VM_IP:8765 MT5_BRIDGE_TOKEN=change-me """ from __future__ import annotations import argparse import json import os from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any from urllib.parse import parse_qs, urlparse import MetaTrader5 as mt5 TIMEFRAMES = { "M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5, "M15": mt5.TIMEFRAME_M15, "M30": mt5.TIMEFRAME_M30, "H1": mt5.TIMEFRAME_H1, "H4": mt5.TIMEFRAME_H4, "D1": mt5.TIMEFRAME_D1, } def json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict[str, Any]) -> None: body = json.dumps(payload, separators=(",", ":")).encode("utf-8") handler.send_response(status) handler.send_header("Content-Type", "application/json") handler.send_header("Content-Length", str(len(body))) handler.end_headers() handler.wfile.write(body) def require_token(handler: BaseHTTPRequestHandler) -> bool: token = os.environ.get("MT5_BRIDGE_TOKEN", "").strip() if not token: return True auth = handler.headers.get("Authorization", "") if auth == f"Bearer {token}": return True json_response(handler, 401, {"error": "unauthorized"}) return False def ensure_symbol(symbol: str) -> bool: selected = mt5.symbol_select(symbol, True) return bool(selected) class MT5Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args: Any) -> None: print(f"[MT5Bridge] {self.address_string()} - {fmt % args}") def do_GET(self) -> None: if not require_token(self): return parsed = urlparse(self.path) query = parse_qs(parsed.query) if parsed.path == "/health": account = mt5.account_info() terminal = mt5.terminal_info() json_response(self, 200, { "ok": account is not None and terminal is not None, "account": account._asdict() if account else None, "terminal": terminal._asdict() if terminal else None, }) return if parsed.path == "/symbols": symbols = mt5.symbols_get() names = [s.name for s in symbols] if symbols else [] json_response(self, 200, {"symbols": names}) return if parsed.path == "/price": symbol = query.get("symbol", [""])[0].strip() if not symbol: json_response(self, 400, {"error": "missing symbol"}) return if not ensure_symbol(symbol): json_response(self, 404, {"error": "symbol not found", "symbol": symbol}) return tick = mt5.symbol_info_tick(symbol) if tick is None: json_response(self, 404, {"error": "no tick", "symbol": symbol}) return bid = float(tick.bid or 0) ask = float(tick.ask or 0) price = ask if ask > 0 else bid if bid > 0 and ask > 0: price = (bid + ask) / 2 json_response(self, 200, { "symbol": symbol, "bid": bid, "ask": ask, "price": price, "time": datetime.fromtimestamp(tick.time, tz=timezone.utc).isoformat(), }) return if parsed.path == "/ohlcv": symbol = query.get("symbol", [""])[0].strip() timeframe_name = query.get("timeframe", ["M15"])[0].strip().upper() count = int(query.get("count", ["160"])[0]) timeframe = TIMEFRAMES.get(timeframe_name) if not symbol or timeframe is None: json_response(self, 400, {"error": "missing symbol or invalid timeframe"}) return if not ensure_symbol(symbol): json_response(self, 404, {"error": "symbol not found", "symbol": symbol}) return rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, max(1, min(count, 1000))) if rates is None: json_response(self, 404, {"error": "no rates", "symbol": symbol, "timeframe": timeframe_name}) return bars = [] for row in rates: bars.append({ "time": datetime.fromtimestamp(int(row["time"]), tz=timezone.utc).isoformat(), "open": float(row["open"]), "high": float(row["high"]), "low": float(row["low"]), "close": float(row["close"]), "volume": float(row["tick_volume"]), }) json_response(self, 200, {"symbol": symbol, "timeframe": timeframe_name, "bars": bars}) return json_response(self, 404, {"error": "not found"}) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--host", default=os.environ.get("MT5_BRIDGE_HOST", "0.0.0.0")) parser.add_argument("--port", type=int, default=int(os.environ.get("MT5_BRIDGE_PORT", "8765"))) args = parser.parse_args() if not mt5.initialize(): raise SystemExit(f"MT5 initialize failed: {mt5.last_error()}") server = ThreadingHTTPServer((args.host, args.port), MT5Handler) print(f"[MT5Bridge] listening on http://{args.host}:{args.port}") try: server.serve_forever() finally: mt5.shutdown() if __name__ == "__main__": main()