Files
mt5-quant/scripts/platform_detect.sh
Devid HW 3f763827f4 feat: MT5-Quant MCP server for backtesting and optimization
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
2026-04-18 11:41:41 +07:00

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