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.
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
# MT5 on Arch Linux (Wine + mt5linux bridge)
|
||||||
|
|
||||||
|
The `MetaTrader5` Python package is Windows-only. On Arch Linux we run the MT5
|
||||||
|
terminal under **Wine** plus a **Windows Python** that exposes the MT5 API over
|
||||||
|
an `rpyc` socket (`mt5linux`). The Linux-side bot connects to that socket.
|
||||||
|
|
||||||
|
```
|
||||||
|
Linux Python (bot) --rpyc--> Wine Python (mt5linux server) --> MT5 terminal --> broker
|
||||||
|
mt5linux client MetaTrader5 pkg terminal64.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## One-time setup (already done on this machine)
|
||||||
|
|
||||||
|
- Wine: `wine-11.10 (Staging)` ✅
|
||||||
|
- Headless display: `Xvfb` (pkg `xorg-server-xvfb`) ✅
|
||||||
|
- MT5 terminal installed in Wine: `~/.wine/drive_c/Program Files/MetaTrader 5/terminal64.exe` ✅
|
||||||
|
- Windows Python 3.9 in Wine: `~/.wine/drive_c/users/$USER/AppData/Local/Programs/Python/Python39/python.exe` ✅
|
||||||
|
- Wine Python packages: `MetaTrader5`, `mt5linux`, `rpyc` ✅
|
||||||
|
- Linux Python packages: `mt5linux`, `rpyc` ✅
|
||||||
|
|
||||||
|
To reproduce the Wine Python install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL -o /tmp/py39.exe https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe
|
||||||
|
DISPLAY=:1 WINEDEBUG=-all wine /tmp/py39.exe /quiet InstallAllUsers=0 PrependPath=1 Include_pip=1
|
||||||
|
WINPY="$HOME/.wine/drive_c/users/$USER/AppData/Local/Programs/Python/Python39/python.exe"
|
||||||
|
wine "$WINPY" -m pip install MetaTrader5 mt5linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration (`.env`)
|
||||||
|
|
||||||
|
```env
|
||||||
|
MT5_LOGIN=...
|
||||||
|
MT5_PASSWORD=...
|
||||||
|
MT5_SERVER=... # exact broker server name (see terminal)
|
||||||
|
MT5_BRIDGE_HOST=127.0.0.1
|
||||||
|
MT5_BRIDGE_PORT=18812
|
||||||
|
MT5_WIN_PATH=C:\Program Files\MetaTrader 5\terminal64.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run (robust, one command)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bring the whole bridge up (idempotent): Xvfb + rpyc server + verified login.
|
||||||
|
# The terminal is auto-launched headless and logged in from .env credentials.
|
||||||
|
scripts/mt5_bridge.sh up
|
||||||
|
|
||||||
|
# Other commands
|
||||||
|
scripts/mt5_bridge.sh status # components + live login + trade_allowed
|
||||||
|
scripts/mt5_bridge.sh restart
|
||||||
|
scripts/mt5_bridge.sh down
|
||||||
|
```
|
||||||
|
|
||||||
|
`up` is safe to re-run: it only starts what is missing and health-checks the
|
||||||
|
rpyc port (no blind sleeps). Credentials and port are read from `.env`
|
||||||
|
(`MT5_LOGIN/PASSWORD/SERVER`, `MT5_BRIDGE_PORT`, `MT5_WIN_PATH`).
|
||||||
|
|
||||||
|
### Auto-start on login (systemd --user)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/mt5_bridge.sh install-service
|
||||||
|
systemctl --user enable --now mt5-bridge.service
|
||||||
|
journalctl --user -u mt5-bridge -f # logs
|
||||||
|
```
|
||||||
|
|
||||||
|
After this the bridge comes up automatically; you never repeat the manual
|
||||||
|
steps. Then just run the bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main_live.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-time only
|
||||||
|
|
||||||
|
The account must be logged into the terminal once so the profile knows it.
|
||||||
|
Normally `up` does this automatically via explicit login. If you ever get
|
||||||
|
`LOGIN_FAIL (-6)`, do a one-time GUI login:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/mt5_bridge.sh login-gui # File -> Login to Trade Account
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot (`src/mt5_connector.py`) auto-detects the bridge: if the native
|
||||||
|
`MetaTrader5` import fails but `mt5linux` is present, it connects lazily to
|
||||||
|
`MT5_BRIDGE_HOST:MT5_BRIDGE_PORT` on the first `connect()` call, trying
|
||||||
|
`initialize()` attach-mode first then explicit login.
|
||||||
|
|
||||||
|
### Zero-step auto-connect
|
||||||
|
|
||||||
|
You do **not** need to start anything by hand. When the bot calls `connect()`
|
||||||
|
and the bridge port is closed, it auto-runs `scripts/mt5_bridge.sh up`
|
||||||
|
(Xvfb + rpyc server + login) and waits for it to become ready. So on Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main_live.py # bridge is started automatically if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable autostart (e.g. when using the systemd service) with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MT5_BRIDGE_AUTOSTART=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Broker symbol note
|
||||||
|
|
||||||
|
XM Global names spot gold **`GOLD`** (not `XAUUSD`). `.env` sets `SYMBOL=GOLD`.
|
||||||
|
The connector pre-selects whatever `SYMBOL` is configured.
|
||||||
|
|
||||||
|
## Root cause of the `-6 Authorization failed` we hit
|
||||||
|
|
||||||
|
Per the official mt5linux docs, the intended flow is:
|
||||||
|
**(1) open MT5 and log in via the GUI → (2) start the server → (3) call
|
||||||
|
`initialize()` with no credentials to attach to the running terminal.**
|
||||||
|
|
||||||
|
Our first attempt launched the terminal with `/portable` and passed
|
||||||
|
`login/password/server` to `initialize()`. A fresh portable profile has **no
|
||||||
|
saved account**, so the broker rejected the fresh login attempt with
|
||||||
|
`(-6, 'Authorization failed')`. Confirmed: no `accounts.dat`/`servers.dat`
|
||||||
|
account was present in the Wine prefix.
|
||||||
|
|
||||||
|
Fixes applied:
|
||||||
|
- launcher no longer uses `/portable` (uses the persistent profile)
|
||||||
|
- added `scripts/mt5_bridge.sh login_gui` for the one-time GUI login
|
||||||
|
- the connector + test now try `initialize()` attach-mode first
|
||||||
|
|
||||||
|
> So the credentials were almost certainly fine — the terminal simply had never
|
||||||
|
> logged into the account. Do the one-time `login_gui` step, then the bridge
|
||||||
|
> attaches cleanly.
|
||||||
|
|
||||||
|
## QA status (verified)
|
||||||
|
|
||||||
|
- rpyc server starts and listens on `127.0.0.1:18812` ✅
|
||||||
|
- Linux client connects through to the broker ✅
|
||||||
|
- `mt5.initialize(...)` reaches the broker server ✅
|
||||||
|
- `src/mt5_connector.py` imports without blocking; backend detected as `bridge` ✅
|
||||||
|
|
||||||
|
> NOTE: With the supplied XM credentials the broker returned
|
||||||
|
> `(-6, 'Authorization failed')`. The transport works end-to-end — this is a
|
||||||
|
> credential/server-name issue, not an infrastructure problem. Confirm in the
|
||||||
|
> MT5 terminal:
|
||||||
|
> - exact **server name** (XM has several, e.g. `XMGlobal-MT5`, `XMGlobal-MT5 2` … `10`)
|
||||||
|
> - **login number** and **password** (investor vs master password)
|
||||||
|
> - account is active and AlgoTrading is enabled
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- `Authorization failed (-6)` → wrong server name / login / password, or
|
||||||
|
expired account. Log into the terminal GUI once to confirm the exact server.
|
||||||
|
- Client `result expired` / timeout → the Wine MT5 terminal hung; run
|
||||||
|
`scripts/mt5_bridge.sh restart`.
|
||||||
|
- Port already in use → another server instance is running; `mt5_bridge.sh stop`.
|
||||||
Executable
+221
@@ -0,0 +1,221 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MT5 Bridge Connection Test (Arch Linux + Wine)
|
||||||
|
==============================================
|
||||||
|
Connects to the mt5linux rpyc server (started by scripts/mt5_bridge.sh),
|
||||||
|
initializes the MetaTrader5 terminal with .env credentials, and prints
|
||||||
|
account + market-data sanity checks.
|
||||||
|
|
||||||
|
Prereq:
|
||||||
|
scripts/mt5_bridge.sh start
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/test_mt5_bridge.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Load .env
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
HOST = os.getenv("MT5_BRIDGE_HOST", "127.0.0.1")
|
||||||
|
PORT = int(os.getenv("MT5_BRIDGE_PORT", "18812"))
|
||||||
|
LOGIN = int(os.getenv("MT5_LOGIN", "0"))
|
||||||
|
PASSWORD = os.getenv("MT5_PASSWORD", "")
|
||||||
|
SERVER = os.getenv("MT5_SERVER", "")
|
||||||
|
# Wine-side (Windows) terminal path used by the rpyc server
|
||||||
|
WIN_PATH = os.getenv("MT5_WIN_PATH", r"C:\Program Files\MetaTrader 5\terminal64.exe")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
from mt5linux import MetaTrader5
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: mt5linux not installed (pip install mt5linux)")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print(f"Connecting to bridge {HOST}:{PORT} ...")
|
||||||
|
try:
|
||||||
|
mt5 = MetaTrader5(host=HOST, port=PORT)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: cannot reach rpyc server: {e}")
|
||||||
|
print("Hint: start it first -> scripts/mt5_bridge.sh start")
|
||||||
|
return 3
|
||||||
|
|
||||||
|
# Preferred (official mt5linux) flow: the terminal is already logged in via
|
||||||
|
# GUI, so initialize() attaches to it without credentials.
|
||||||
|
print("initialize() [attach to already-logged-in terminal] ...")
|
||||||
|
ok = mt5.initialize()
|
||||||
|
if not ok:
|
||||||
|
print(f" attach failed -> {mt5.last_error()}")
|
||||||
|
# Fallback: explicit login (works only if account/server are valid AND
|
||||||
|
# the terminal already knows this account).
|
||||||
|
print(f"initialize(login={LOGIN}, server={SERVER!r}) [explicit login] ...")
|
||||||
|
ok = mt5.initialize(login=LOGIN, password=PASSWORD, server=SERVER, path=WIN_PATH)
|
||||||
|
if not ok:
|
||||||
|
print(f"FAILED: initialize -> {mt5.last_error()}")
|
||||||
|
print("Fix: log in once via GUI so the terminal remembers the account:")
|
||||||
|
print(" scripts/mt5_bridge.sh login_gui")
|
||||||
|
print("Also verify server name, login, and MASTER password (not investor).")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("initialize OK")
|
||||||
|
ai = mt5.account_info()
|
||||||
|
if ai is not None:
|
||||||
|
print(f" account : {ai.login} | balance={ai.balance} {ai.currency} | server={ai.server}")
|
||||||
|
ti = mt5.terminal_info()
|
||||||
|
if ti is not None:
|
||||||
|
print(f" terminal: connected={ti.connected} trade_allowed={ti.trade_allowed}")
|
||||||
|
|
||||||
|
symbol = os.getenv("SYMBOL", "XAUUSD")
|
||||||
|
rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_M15, 0, 5)
|
||||||
|
if rates is not None and len(rates) > 0:
|
||||||
|
print(f" rates : fetched {len(rates)} {symbol} M15 bars OK")
|
||||||
|
else:
|
||||||
|
print(f" rates : WARN no rates for {symbol} -> {mt5.last_error()}")
|
||||||
|
|
||||||
|
mt5.shutdown()
|
||||||
|
print("DONE: bridge fully functional")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+122
-6
@@ -11,14 +11,111 @@ import numpy as np
|
|||||||
from typing import Optional, Dict, Any, List, Tuple
|
from typing import Optional, Dict, Any, List, Tuple
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
import time
|
import time
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# MT5 backend resolution:
|
||||||
|
# 1) Native MetaTrader5 (Windows / Wine Python directly)
|
||||||
|
# 2) mt5linux rpyc bridge (Linux client -> Wine Python server)
|
||||||
|
# The bridge client is created lazily via get_mt5_backend() so that importing
|
||||||
|
# this module never blocks on a network connection.
|
||||||
|
mt5 = None
|
||||||
|
_MT5_BACKEND = "none"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import MetaTrader5 as mt5
|
import MetaTrader5 as mt5 # type: ignore
|
||||||
|
_MT5_BACKEND = "native"
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("MetaTrader5 not installed. Running in simulation mode.")
|
try:
|
||||||
mt5 = None
|
import mt5linux # type: ignore # noqa: F401
|
||||||
|
_MT5_BACKEND = "bridge" # available, connect lazily in get_mt5_backend()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
_MT5_BACKEND = "none"
|
||||||
|
|
||||||
|
|
||||||
|
def _bridge_port_open(host: str, port: int, timeout: float = 1.0) -> bool:
|
||||||
|
"""Return True if something is listening on host:port."""
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=timeout):
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_bridge_running(host: str, port: int) -> bool:
|
||||||
|
"""Best-effort: start the MT5 bridge (Xvfb + rpyc server) if it's down.
|
||||||
|
|
||||||
|
Lets the bot auto-connect without the user launching anything manually.
|
||||||
|
Only attempts a local autostart when host is loopback. Returns True if the
|
||||||
|
port is open afterwards.
|
||||||
|
"""
|
||||||
|
if _bridge_port_open(host, port):
|
||||||
|
return True
|
||||||
|
if host not in ("127.0.0.1", "localhost", "::1"):
|
||||||
|
return False # remote bridge: not ours to start
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
script = Path(__file__).resolve().parent.parent / "scripts" / "mt5_bridge.sh"
|
||||||
|
if not script.exists():
|
||||||
|
logger.warning(f"MT5 bridge autostart skipped: {script} not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("MT5 bridge down -> auto-starting via scripts/mt5_bridge.sh up ...")
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["bash", str(script), "up"],
|
||||||
|
timeout=150,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning(f"MT5 bridge autostart error: {e}")
|
||||||
|
|
||||||
|
# Wait for the port to come up
|
||||||
|
for _ in range(30):
|
||||||
|
if _bridge_port_open(host, port):
|
||||||
|
logger.info("MT5 bridge is up.")
|
||||||
|
return True
|
||||||
|
time.sleep(1)
|
||||||
|
logger.warning("MT5 bridge did not come up; falling back to simulation.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_mt5_backend():
|
||||||
|
"""Return an MT5 API object, connecting the rpyc bridge on first use.
|
||||||
|
|
||||||
|
On Linux this will auto-start the Wine bridge (scripts/mt5_bridge.sh up)
|
||||||
|
if it is not already running, so the bot connects with no manual steps.
|
||||||
|
Returns None if no backend is available (simulation mode).
|
||||||
|
"""
|
||||||
|
global mt5
|
||||||
|
if mt5 is not None:
|
||||||
|
return mt5
|
||||||
|
if _MT5_BACKEND == "bridge":
|
||||||
|
host = os.getenv("MT5_BRIDGE_HOST", "127.0.0.1")
|
||||||
|
port = int(os.getenv("MT5_BRIDGE_PORT", "18812"))
|
||||||
|
# Auto-start the bridge if it isn't already listening (opt-out via env).
|
||||||
|
if os.getenv("MT5_BRIDGE_AUTOSTART", "1") not in ("0", "false", "False"):
|
||||||
|
_ensure_bridge_running(host, port)
|
||||||
|
try:
|
||||||
|
from mt5linux import MetaTrader5 as _MT5Bridge # type: ignore
|
||||||
|
mt5 = _MT5Bridge(host=host, port=port)
|
||||||
|
logger.info(f"MetaTrader5 via mt5linux bridge {host}:{port}")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning(
|
||||||
|
f"MT5 bridge connect failed ({host}:{port}): {e}. "
|
||||||
|
"Simulation mode."
|
||||||
|
)
|
||||||
|
mt5 = None
|
||||||
|
elif _MT5_BACKEND == "none":
|
||||||
|
logger.warning("MetaTrader5 unavailable. Running in simulation mode.")
|
||||||
|
return mt5
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -127,6 +224,9 @@ class MT5Connector:
|
|||||||
Returns:
|
Returns:
|
||||||
True if connected successfully
|
True if connected successfully
|
||||||
"""
|
"""
|
||||||
|
global mt5
|
||||||
|
if mt5 is None:
|
||||||
|
mt5 = get_mt5_backend()
|
||||||
if mt5 is None:
|
if mt5 is None:
|
||||||
logger.warning("MT5 not available - simulation mode")
|
logger.warning("MT5 not available - simulation mode")
|
||||||
return False
|
return False
|
||||||
@@ -150,7 +250,21 @@ class MT5Connector:
|
|||||||
if self.path:
|
if self.path:
|
||||||
kwargs["path"] = self.path
|
kwargs["path"] = self.path
|
||||||
|
|
||||||
if mt5.initialize(**kwargs):
|
# On the mt5linux/Wine bridge the terminal is typically already
|
||||||
|
# logged in via GUI; initialize() without credentials attaches
|
||||||
|
# to it. Try attach-mode first, then fall back to explicit login.
|
||||||
|
_initialized = False
|
||||||
|
if _MT5_BACKEND == "bridge":
|
||||||
|
try:
|
||||||
|
_initialized = mt5.initialize()
|
||||||
|
except Exception:
|
||||||
|
_initialized = False
|
||||||
|
if not _initialized:
|
||||||
|
_initialized = mt5.initialize(**kwargs)
|
||||||
|
else:
|
||||||
|
_initialized = mt5.initialize(**kwargs)
|
||||||
|
|
||||||
|
if _initialized:
|
||||||
# Wait for terminal to fully initialize
|
# Wait for terminal to fully initialize
|
||||||
time.sleep(2) # Increased delay for stability
|
time.sleep(2) # Increased delay for stability
|
||||||
|
|
||||||
@@ -176,8 +290,10 @@ class MT5Connector:
|
|||||||
self._account_info = self._get_account_info()
|
self._account_info = self._get_account_info()
|
||||||
logger.info(f"Connected to MT5: {self.server} (Account: {self.login})")
|
logger.info(f"Connected to MT5: {self.server} (Account: {self.login})")
|
||||||
|
|
||||||
# Pre-select common symbols to ensure they're ready
|
# Pre-select the configured trading symbol so it's ready.
|
||||||
mt5.symbol_select("XAUUSD", True)
|
# Broker-specific name (e.g. XM uses 'GOLD', not 'XAUUSD').
|
||||||
|
_sym = os.getenv("SYMBOL", "XAUUSD")
|
||||||
|
mt5.symbol_select(_sym, True)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
+1
-1
@@ -178,7 +178,7 @@ class NewsAgent:
|
|||||||
try:
|
try:
|
||||||
# Try to get calendar events (broker-dependent)
|
# Try to get calendar events (broker-dependent)
|
||||||
# Some brokers don't expose this API
|
# Some brokers don't expose this API
|
||||||
events = mt5.copy_ticks_from("XAUUSD", now - timedelta(hours=1), 1, mt5.COPY_TICKS_INFO)
|
events = mt5.copy_ticks_from(os.getenv("SYMBOL", "XAUUSD"), now - timedelta(hours=1), 1, mt5.COPY_TICKS_INFO)
|
||||||
# If we get here, try calendar
|
# If we get here, try calendar
|
||||||
calendar_events = []
|
calendar_events = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user