Files
Vanszs 303fdfa689 feat: Linux (Arch+Wine) MT5 support with zero-step auto-connect
Make the bot connect to MetaTrader5 on Linux without manual setup or
launching extra programs.

- Add mt5linux rpyc bridge: MT5 terminal + Windows Python run under Wine,
  Linux client talks to it over a socket
- scripts/mt5_bridge.sh: robust, idempotent launcher (up/down/status/restart/
  login-gui/install-service) with .env loading, port health-checks and a
  systemd --user unit for auto-start on login
- src/mt5_connector.py: auto-detect bridge backend (lazy, non-blocking import);
  auto-start the bridge via mt5_bridge.sh when the port is down so the bot
  self-connects; attach-mode initialize() first, then explicit login
- Use configured SYMBOL instead of hardcoded XAUUSD in connector + news_agent
  (XM names spot gold 'GOLD')
- scripts/test_mt5_bridge.py: connectivity smoke test
- docs/MT5-ARCH-LINUX-SETUP.md: full setup, root-cause notes, troubleshooting

Verified live: account 345454551 @ XMGlobal-MT5 10, balance read, GOLD M15
data + ticks streaming, trade_allowed=True.
2026-06-06 14:01:38 +07:00

222 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# MT5 Bridge (Arch Linux + Wine) — robust, idempotent, headless
# =============================================================================
# One command brings the MT5 API up on Linux:
#
# scripts/mt5_bridge.sh up # ensure Xvfb + rpyc server are running,
# # then verify a live MT5 login (auto)
# scripts/mt5_bridge.sh down # stop server + Xvfb (+ stray terminal)
# scripts/mt5_bridge.sh status # show component + login status
# scripts/mt5_bridge.sh restart
# scripts/mt5_bridge.sh login-gui # first-time GUI login (rarely needed)
# scripts/mt5_bridge.sh install-service # install systemd --user unit
#
# Design:
# - The rpyc server runs under Wine Python and exposes the MetaTrader5 API.
# - The bot (or this script's healthcheck) calls initialize(login,...) which
# auto-launches the terminal headless under Xvfb and logs in. No GUI needed
# after the account has been logged in once.
# - Idempotent: re-running `up` only starts what is missing.
# - Readiness is health-checked (socket + rpyc), not fixed sleeps.
#
# Config: auto-sourced from the project .env (MT5_LOGIN/PASSWORD/SERVER,
# MT5_BRIDGE_PORT, MT5_WIN_PATH). Override via env vars.
# =============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# --- Load .env (robust: only KEY=value lines, supports quoted values) --------
if [ -f "$PROJECT_DIR/.env" ]; then
while IFS= read -r _line || [ -n "$_line" ]; do
case "$_line" in
''|\#*) continue ;; # skip blank / comment
[A-Za-z_]*=*) eval "export $_line" 2>/dev/null || true ;;
esac
done < "$PROJECT_DIR/.env"
fi
PORT="${MT5_BRIDGE_PORT:-18812}"
DISP="${MT5_BRIDGE_DISPLAY:-:99}"
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine}"
export WINEDEBUG="${WINEDEBUG:--all}"
WINPY="$WINEPREFIX/drive_c/users/$USER/AppData/Local/Programs/Python/Python39/python.exe"
MT5_TERM="$WINEPREFIX/drive_c/Program Files/MetaTrader 5/terminal64.exe"
WIN_PATH="${MT5_WIN_PATH:-C:\\Program Files\\MetaTrader 5\\terminal64.exe}"
RUN_DIR="${XDG_RUNTIME_DIR:-/tmp}/xaubot-mt5"
mkdir -p "$RUN_DIR"
XVFB_PID="$RUN_DIR/xvfb.pid"
SRV_PID="$RUN_DIR/server.pid"
LOG="$RUN_DIR/bridge.log"
log() { echo "[mt5-bridge] $*"; }
err() { echo "[mt5-bridge] ERROR: $*" >&2; }
_check_prereq() {
local ok=1
command -v wine >/dev/null || { err "wine not found"; ok=0; }
command -v Xvfb >/dev/null || { err "Xvfb not found (pacman -S xorg-server-xvfb)"; ok=0; }
[ -f "$WINPY" ] || { err "Wine Python missing: $WINPY"; ok=0; }
[ -f "$MT5_TERM" ] || { err "MT5 terminal missing: $MT5_TERM"; ok=0; }
python3 -c "import mt5linux" 2>/dev/null || { err "linux pkg 'mt5linux' missing (pip install mt5linux)"; ok=0; }
[ "$ok" = 1 ] || exit 1
}
_alive() { [ -f "$1" ] && kill -0 "$(cat "$1")" 2>/dev/null; }
_port_open() { (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null && exec 3>&- ; }
# Wait until the rpyc port accepts connections (timeout seconds)
_wait_port() {
local t="${1:-20}"
for _ in $(seq 1 "$t"); do _port_open && return 0; sleep 1; done
return 1
}
_ensure_xvfb() {
if _alive "$XVFB_PID"; then return 0; fi
# Reuse an existing Xvfb on $DISP if present
if pgrep -f "Xvfb $DISP" >/dev/null 2>&1; then
pgrep -f "Xvfb $DISP" | head -1 > "$XVFB_PID"
log "Reusing Xvfb on $DISP"
return 0
fi
setsid Xvfb "$DISP" -screen 0 1280x1024x24 -nolisten tcp >>"$LOG" 2>&1 &
echo $! > "$XVFB_PID"
sleep 1
log "Xvfb started on $DISP (pid $(cat "$XVFB_PID"))"
}
_ensure_server() {
if _alive "$SRV_PID" && _port_open; then return 0; fi
if _port_open; then
log "rpyc server already listening on :$PORT (external)"
return 0
fi
log "Starting rpyc server (Wine Python) on 127.0.0.1:$PORT ..."
setsid env DISPLAY="$DISP" WINEDEBUG="$WINEDEBUG" \
wine "$WINPY" -m mt5linux --host 127.0.0.1 -p "$PORT" >>"$LOG" 2>&1 &
echo $! > "$SRV_PID"
if _wait_port 25; then
log "rpyc server UP (pid $(cat "$SRV_PID"))"
else
err "rpyc server failed to open port :$PORT (see $LOG)"
exit 1
fi
}
# Verify a live login through the bridge (auto-launches terminal headless).
_verify_login() {
DISPLAY="$DISP" MT5_BRIDGE_PORT="$PORT" python3 - "$WIN_PATH" <<'PY'
import os, sys
from mt5linux import MetaTrader5
port = int(os.getenv("MT5_BRIDGE_PORT", "18812"))
win_path = sys.argv[1]
login = int(os.getenv("MT5_LOGIN", "0"))
pw = os.getenv("MT5_PASSWORD", "")
srv = os.getenv("MT5_SERVER", "")
try:
m = MetaTrader5(host="127.0.0.1", port=port, timeout=120)
except Exception as e:
print(f"CONNECT_FAIL {e}"); sys.exit(3)
# attach first, then explicit login (auto-launches terminal via path)
ok = m.initialize()
if not ok:
ok = m.initialize(login=login, password=pw, server=srv, path=win_path)
if not ok:
print(f"LOGIN_FAIL {m.last_error()}"); sys.exit(1)
ai = m.account_info()
if ai:
print(f"OK login={ai.login} balance={ai.balance} {ai.currency} server={ai.server}")
ti = m.terminal_info()
if ti:
print(f"TERMINAL connected={ti.connected} trade_allowed={ti.trade_allowed}")
sys.exit(0)
PY
}
up() {
_check_prereq
_ensure_xvfb
_ensure_server
log "Verifying MT5 login ..."
if out="$(_verify_login)"; then
echo "$out" | sed 's/^/[mt5-bridge] /'
log "Bridge READY (port=$PORT display=$DISP)"
echo "$out" | grep -q "trade_allowed=False" && \
log "WARNING: AlgoTrading OFF in terminal -> bot cannot place orders. Enable 'Algo Trading' in MT5."
return 0
else
err "Login verification failed:"
echo "$out" | sed 's/^/[mt5-bridge] /'
err "If LOGIN_FAIL (-6): run 'scripts/mt5_bridge.sh login-gui' once."
return 1
fi
}
down() {
for p in "$SRV_PID" "$XVFB_PID"; do
if _alive "$p"; then kill "$(cat "$p")" 2>/dev/null || true; log "stopped pid $(cat "$p")"; fi
rm -f "$p"
done
pkill -f "mt5linux" 2>/dev/null || true
pkill -f "terminal64.exe" 2>/dev/null || true
log "Bridge down."
}
status() {
_alive "$XVFB_PID" && log "Xvfb : UP ($(cat "$XVFB_PID"))" || log "Xvfb : DOWN"
if _port_open; then log "rpyc : UP (:$PORT listening)"; else log "rpyc : DOWN"; fi
if _port_open; then
log "Login check:"
_verify_login 2>/dev/null | sed 's/^/[mt5-bridge] /' || log " (login check failed)"
fi
log "port=$PORT display=$DISP run_dir=$RUN_DIR"
}
login_gui() {
_check_prereq
local d="${DISPLAY:-:0}"
log "Opening MT5 on DISPLAY=$d. In MT5: File -> Login to Trade Account"
log " Login=${MT5_LOGIN:-?} Server=${MT5_SERVER:-?}"
log "Close the terminal after 'authorized'. Then run: scripts/mt5_bridge.sh up"
DISPLAY="$d" WINEDEBUG=-all wine "$MT5_TERM"
}
install_service() {
local unit_dir="$HOME/.config/systemd/user"
mkdir -p "$unit_dir"
cat > "$unit_dir/mt5-bridge.service" <<EOF
[Unit]
Description=XauBot MT5 Bridge (Wine + mt5linux)
After=graphical-session.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=$PROJECT_DIR
ExecStart=$SCRIPT_DIR/mt5_bridge.sh up
ExecStop=$SCRIPT_DIR/mt5_bridge.sh down
TimeoutStartSec=120
[Install]
WantedBy=default.target
EOF
log "Installed: $unit_dir/mt5-bridge.service"
log "Enable on login: systemctl --user enable --now mt5-bridge.service"
log "Logs: journalctl --user -u mt5-bridge -f"
}
case "${1:-}" in
up) up ;;
down|stop) down ;;
restart) down; sleep 2; up ;;
status) status ;;
login-gui|login_gui) login_gui ;;
install-service) install_service ;;
*) echo "Usage: $0 {up|down|restart|status|login-gui|install-service}"; exit 1 ;;
esac