3f763827f4
MCP server exposing MetaTrader 5 strategy development tools to AI assistants (Claude, Cursor, etc.) on macOS (CrossOver) and Linux (Wine). Tools: - run_backtest: full pipeline — compile EA, clean cache, backtest, parse HTML/XML report, analyze deals → metrics.json + analysis.json - run_optimization: background genetic optimization with nohup/disown, UTF-16LE .set file handling, OptMode reset - compile_ea: MQL5 compilation via MetaEditor with auto-detected include/ directory sync - get_backtest_status / get_optimization_status: job polling - verify_environment: Wine/MT5 path validation Analytics: - extract.py: MT5 HTML and SpreadsheetML XML report parser - analyze.py: deal-level analysis (drawdown events, grid depth, loss sequences, monthly P&L) → analysis.json - optimize_parser.py: optimization result parser with convergence analysis Platform support: - macOS CrossOver (GUI mode, no Xvfb needed) - Linux Wine + Xvfb (headless, CI/CD compatible) - Auto-detection of Wine executable and MT5 terminal paths
245 lines
8.6 KiB
Bash
Executable File
245 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# platform_detect.sh — Detect Wine path, MT5 location, and display mode
|
|
# Sourced by other scripts: source scripts/platform_detect.sh
|
|
# Sets: MT5_WINE, MT5_DIR, MT5_EXPERTS_DIR, MT5_TESTER_DIR, MT5_CACHE_DIR, DISPLAY_ENV
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# Config resolution: user config (~/.config/mt5-quant/) takes precedence over repo config
|
|
_USER_CFG="${HOME}/.config/mt5-quant/config/mt5-quant.yaml"
|
|
_REPO_CFG="${SCRIPT_DIR}/../config/mt5-quant.yaml"
|
|
if [[ -f "$_USER_CFG" ]]; then
|
|
CONFIG_FILE="$_USER_CFG"
|
|
elif [[ -f "$_REPO_CFG" ]]; then
|
|
CONFIG_FILE="$_REPO_CFG"
|
|
else
|
|
CONFIG_FILE="$_REPO_CFG" # will fail gracefully in _cfg
|
|
fi
|
|
|
|
# ── Config reader (minimal YAML parser for simple key: value) ────────────────
|
|
_cfg() {
|
|
local key="$1"
|
|
local default="${2:-}"
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
local val
|
|
val=$(grep -E "^[[:space:]]*${key}[[:space:]]*:" "$CONFIG_FILE" 2>/dev/null \
|
|
| head -1 | sed 's/.*:[[:space:]]*//' | tr -d '"' | tr -d "'" | tr -d '\r')
|
|
if [[ -n "$val" && "$val" != "null" && "$val" != '""' ]]; then
|
|
echo "$val"
|
|
return
|
|
fi
|
|
fi
|
|
echo "$default"
|
|
}
|
|
|
|
# ── Wine / CrossOver detection ───────────────────────────────────────────────
|
|
_detect_wine() {
|
|
local configured
|
|
configured=$(_cfg "wine_executable")
|
|
|
|
if [[ -n "$configured" && -x "$configured" ]]; then
|
|
echo "$configured"
|
|
return 0
|
|
fi
|
|
|
|
# Auto-detect: native MT5.app (macOS App Store / direct download)
|
|
local mt5_app_wine="/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64"
|
|
if [[ -x "$mt5_app_wine" ]]; then
|
|
echo "$mt5_app_wine"
|
|
return 0
|
|
fi
|
|
|
|
# Auto-detect: CrossOver (macOS)
|
|
local crossover_wine="/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine64"
|
|
if [[ -x "$crossover_wine" ]]; then
|
|
echo "$crossover_wine"
|
|
return 0
|
|
fi
|
|
|
|
# Auto-detect: Wine (Linux / Homebrew)
|
|
for candidate in wine64 wine; do
|
|
if command -v "$candidate" &>/dev/null; then
|
|
echo "$(command -v "$candidate")"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
return 1
|
|
}
|
|
|
|
# ── MT5 terminal directory detection ─────────────────────────────────────────
|
|
_detect_mt5_dir() {
|
|
local configured
|
|
configured=$(_cfg "terminal_dir")
|
|
[[ -n "$configured" && -d "$configured" ]] && { echo "$configured"; return 0; }
|
|
|
|
# macOS CrossOver — scan all bottles
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
# Native MT5.app sandboxed Wine prefix (most common on macOS)
|
|
local app_support="$HOME/Library/Application Support"
|
|
for prefix_pattern in \
|
|
"net.metaquotes.wine.metatrader5" \
|
|
"MetaTrader 5/Bottles/metatrader5"; do
|
|
local candidate="${app_support}/${prefix_pattern}/drive_c/Program Files/MetaTrader 5"
|
|
if [[ -f "${candidate}/terminal64.exe" ]]; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# CrossOver old-style ~/.cxoffice bottles
|
|
local bottle_base="$HOME/.cxoffice"
|
|
if [[ -d "$bottle_base" ]]; then
|
|
while IFS= read -r -d '' terminal; do
|
|
echo "$(dirname "$terminal")"
|
|
return 0
|
|
done < <(find "$bottle_base" -name "terminal64.exe" -print0 2>/dev/null | head -z -1)
|
|
fi
|
|
# CrossOver 24+ default location
|
|
local mq_base="$HOME/Library/Application Support/MetaQuotes"
|
|
if [[ -d "$mq_base" ]]; then
|
|
while IFS= read -r -d '' terminal; do
|
|
echo "$(dirname "$terminal")"
|
|
return 0
|
|
done < <(find "$mq_base" -name "terminal64.exe" -print0 2>/dev/null | head -z -1)
|
|
fi
|
|
fi
|
|
|
|
# Linux Wine — common default
|
|
local wine_mt5="$HOME/.wine/drive_c/Program Files/MetaTrader 5"
|
|
[[ -d "$wine_mt5" ]] && { echo "$wine_mt5"; return 0; }
|
|
|
|
echo ""
|
|
return 1
|
|
}
|
|
|
|
# ── Display / headless mode ───────────────────────────────────────────────────
|
|
_detect_display_env() {
|
|
local mode
|
|
mode=$(_cfg "display_mode" "auto")
|
|
local xvfb_display
|
|
xvfb_display=$(_cfg "xvfb_display" ":99")
|
|
local xvfb_screen
|
|
xvfb_screen=$(_cfg "xvfb_screen" "1024x768x16")
|
|
|
|
case "$mode" in
|
|
false|gui)
|
|
# GUI mode — use whatever DISPLAY is set
|
|
echo "gui"
|
|
return 0
|
|
;;
|
|
true|headless)
|
|
# Force headless via Xvfb
|
|
_start_xvfb "$xvfb_display" "$xvfb_screen"
|
|
echo "headless:${xvfb_display}"
|
|
return 0
|
|
;;
|
|
auto|*)
|
|
# macOS: CrossOver handles display — no Xvfb needed
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
echo "gui"
|
|
return 0
|
|
fi
|
|
# Linux: if $DISPLAY is set, use it; otherwise start Xvfb
|
|
if [[ -n "${DISPLAY:-}" ]]; then
|
|
echo "gui"
|
|
return 0
|
|
fi
|
|
_start_xvfb "$xvfb_display" "$xvfb_screen"
|
|
echo "headless:${xvfb_display}"
|
|
return 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
_start_xvfb() {
|
|
local display="$1"
|
|
local screen="$2"
|
|
|
|
if ! command -v Xvfb &>/dev/null; then
|
|
echo "[platform_detect] ERROR: headless mode requires Xvfb. Install with:" >&2
|
|
echo " sudo apt install xvfb # Debian/Ubuntu" >&2
|
|
echo " sudo yum install xorg-x11-server-Xvfb # RHEL/CentOS" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Check if already running
|
|
if xdpyinfo -display "$display" &>/dev/null 2>&1; then
|
|
return 0 # already up
|
|
fi
|
|
|
|
Xvfb "$display" -screen 0 "$screen" &>/dev/null &
|
|
local xvfb_pid=$!
|
|
sleep 1 # brief wait for Xvfb to initialize
|
|
|
|
if ! xdpyinfo -display "$display" &>/dev/null 2>&1; then
|
|
echo "[platform_detect] ERROR: Xvfb failed to start on display ${display}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[platform_detect] Xvfb started (pid=${xvfb_pid}, display=${display})" >&2
|
|
}
|
|
|
|
# ── macOS arch flag ───────────────────────────────────────────────────────────
|
|
_arch_prefix() {
|
|
if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then
|
|
echo "arch -x86_64"
|
|
else
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# ── Wine path converter (host path → Windows C:\ path) ───────────────────────
|
|
host_to_wine_path() {
|
|
local host_path="$1"
|
|
# Convert Unix absolute path to Wine C:\ equivalent
|
|
# Works for both CrossOver and standard Wine
|
|
echo "$host_path" | sed \
|
|
-e 's|.*drive_c/|C:\\|' \
|
|
-e 's|/|\\|g'
|
|
}
|
|
|
|
# ── Main: resolve everything and export ───────────────────────────────────────
|
|
resolve_platform() {
|
|
MT5_WINE=$(_detect_wine) || {
|
|
echo "[platform_detect] ERROR: Wine/CrossOver not found." >&2
|
|
echo " Configure wine_executable in config/mt5-quant.yaml" >&2
|
|
echo " macOS: install CrossOver from https://www.codeweavers.com/" >&2
|
|
echo " Linux: sudo apt install wine64" >&2
|
|
exit 1
|
|
}
|
|
|
|
MT5_DIR=$(_detect_mt5_dir) || {
|
|
echo "[platform_detect] ERROR: MetaTrader 5 installation not found." >&2
|
|
echo " Configure terminal_dir in config/mt5-quant.yaml" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Derive sub-paths from MT5_DIR (override via config if needed)
|
|
MT5_EXPERTS_DIR="$(_cfg "experts_dir" "${MT5_DIR}/MQL5/Experts")"
|
|
MT5_TESTER_DIR="$(_cfg "tester_profiles_dir" "${MT5_DIR}/MQL5/Profiles/Tester")"
|
|
MT5_CACHE_DIR="$(_cfg "tester_cache_dir" "${MT5_DIR}/Tester")"
|
|
MT5_ARCH="$(_arch_prefix)"
|
|
|
|
DISPLAY_MODE=$(_detect_display_env)
|
|
if [[ "$DISPLAY_MODE" == headless:* ]]; then
|
|
export DISPLAY="${DISPLAY_MODE#headless:}"
|
|
fi
|
|
|
|
export MT5_WINE MT5_DIR MT5_EXPERTS_DIR MT5_TESTER_DIR MT5_CACHE_DIR MT5_ARCH DISPLAY_MODE
|
|
}
|
|
|
|
# Run if executed directly (not sourced)
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
resolve_platform
|
|
echo "Wine: $MT5_WINE"
|
|
echo "MT5 dir: $MT5_DIR"
|
|
echo "Experts: $MT5_EXPERTS_DIR"
|
|
echo "Tester: $MT5_TESTER_DIR"
|
|
echo "Cache: $MT5_CACHE_DIR"
|
|
echo "Display: $DISPLAY_MODE"
|
|
echo "Arch: ${MT5_ARCH:-native}"
|
|
fi
|