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
227 lines
8.4 KiB
Bash
Executable File
227 lines
8.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# optimize.sh — Launch MT5 genetic optimization (always background + detached)
|
|
#
|
|
# Usage:
|
|
# ./scripts/optimize.sh [options]
|
|
#
|
|
# Options:
|
|
# --expert NAME EA name
|
|
# --set FILE Optimization .set file (with ||Y flags)
|
|
# --symbol SYMBOL Trading symbol
|
|
# --from YYYY.MM.DD Start date
|
|
# --to YYYY.MM.DD End date
|
|
# --deposit AMOUNT Initial deposit
|
|
# --model 0|1|2 Tick model (ALWAYS use 0 for grid/martingale EAs)
|
|
# --log FILE Log file path (default: /tmp/mt5opt_TIMESTAMP.log)
|
|
#
|
|
# IMPORTANT: This script launches MT5 as a detached background process.
|
|
# It returns immediately. Do NOT set a timeout on this script.
|
|
# Monitor /tmp/mt5opt_*.log and wait for user signal before parsing results.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
source "${SCRIPT_DIR}/platform_detect.sh"
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────────────
|
|
DEFAULT_SYMBOL=$(_cfg "backtest_symbol" "XAUUSD")
|
|
DEFAULT_DEPOSIT=$(_cfg "backtest_deposit" "10000")
|
|
DEFAULT_CURRENCY=$(_cfg "backtest_currency" "USD")
|
|
DEFAULT_LEVERAGE=$(_cfg "backtest_leverage" "500")
|
|
|
|
EXPERT=""
|
|
SET_FILE=""
|
|
SYMBOL="$DEFAULT_SYMBOL"
|
|
FROM_DATE=""
|
|
TO_DATE=""
|
|
DEPOSIT="$DEFAULT_DEPOSIT"
|
|
CURRENCY="$DEFAULT_CURRENCY"
|
|
LEVERAGE="$DEFAULT_LEVERAGE"
|
|
MODEL=0 # ALWAYS 0 for optimization — see below
|
|
LOG_FILE=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--expert) EXPERT="$2"; shift 2 ;;
|
|
--set) SET_FILE="$2"; shift 2 ;;
|
|
--symbol) SYMBOL="$2"; shift 2 ;;
|
|
--from) FROM_DATE="$2"; shift 2 ;;
|
|
--to) TO_DATE="$2"; shift 2 ;;
|
|
--deposit) DEPOSIT="$2"; shift 2 ;;
|
|
--model)
|
|
# Warn if user tries to use model != 0
|
|
if [[ "$2" != "0" ]]; then
|
|
echo "WARNING: --model $2 ignored. Optimization always uses model=0." >&2
|
|
echo " Model 1/2 overfits martingale/grid EAs (intra-bar price not simulated)." >&2
|
|
fi
|
|
shift 2
|
|
;;
|
|
--log) LOG_FILE="$2"; shift 2 ;;
|
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$EXPERT" ]] && { echo "ERROR: --expert is required" >&2; exit 1; }
|
|
[[ -z "$SET_FILE" ]] && { echo "ERROR: --set is required" >&2; exit 1; }
|
|
[[ -z "$FROM_DATE" ]] && { echo "ERROR: --from is required" >&2; exit 1; }
|
|
[[ -z "$TO_DATE" ]] && { echo "ERROR: --to is required" >&2; exit 1; }
|
|
|
|
[[ ! -f "$SET_FILE" ]] && { echo "ERROR: Set file not found: $SET_FILE" >&2; exit 1; }
|
|
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
LOG_FILE="${LOG_FILE:-/tmp/mt5opt_${TIMESTAMP}.log}"
|
|
JOB_ID="opt_${TIMESTAMP}"
|
|
|
|
# ── Resolve platform ──────────────────────────────────────────────────────────
|
|
resolve_platform
|
|
|
|
# ── Write .set file as UTF-16LE with BOM (read-only) ─────────────────────────
|
|
# MT5 REQUIREMENT: optimization .set files must be UTF-16LE with BOM.
|
|
# If provided as UTF-8, MT5 strips the ||Y optimization flags silently —
|
|
# every pass runs with the fixed base value and optimization is useless.
|
|
python3 - << PYEOF
|
|
import sys, os, shutil
|
|
|
|
src = "${SET_FILE}"
|
|
dst = "${MT5_TESTER_DIR}/${EXPERT}.set"
|
|
os.makedirs("${MT5_TESTER_DIR}", exist_ok=True)
|
|
|
|
with open(src, 'r', encoding='utf-8', errors='replace') as f:
|
|
content = f.read()
|
|
|
|
# Write UTF-16LE with BOM
|
|
with open(dst, 'w', encoding='utf-16-le') as f:
|
|
f.write('\ufeff') # BOM
|
|
f.write(content)
|
|
|
|
# Make read-only — prevents MT5 from overwriting ||Y flags during optimization
|
|
os.chmod(dst, 0o444)
|
|
print(f" .set → {dst} (UTF-16LE, read-only)")
|
|
PYEOF
|
|
|
|
# ── Reset OptMode in terminal.ini ─────────────────────────────────────────────
|
|
# After any optimization run (complete or aborted), MT5 writes OptMode=-1.
|
|
# On next launch, MT5 reads OptMode=-1 and exits immediately without running.
|
|
# Must reset to 0 before every optimization launch.
|
|
TERMINAL_INI="${MT5_DIR}/terminal.ini"
|
|
if [[ -f "$TERMINAL_INI" ]]; then
|
|
# Use Python for safe in-place edit (sed -i behaves differently on macOS vs Linux)
|
|
python3 - << PYEOF
|
|
import re
|
|
|
|
ini_path = "${TERMINAL_INI}"
|
|
with open(ini_path, 'r', errors='replace') as f:
|
|
content = f.read()
|
|
|
|
# Reset OptMode
|
|
content = re.sub(r'OptMode=.*', 'OptMode=0', content)
|
|
# Remove LastOptimization (causes MT5 to skip running)
|
|
content = re.sub(r'LastOptimization=.*\n?', '', content)
|
|
|
|
with open(ini_path, 'w') as f:
|
|
f.write(content)
|
|
print(f" terminal.ini: OptMode reset to 0")
|
|
PYEOF
|
|
fi
|
|
|
|
# ── Build optimization INI ────────────────────────────────────────────────────
|
|
WINE_PREFIX_DIR=$(dirname "$(dirname "$MT5_DIR")")
|
|
|
|
cat > "${WINE_PREFIX_DIR}/drive_c/mt5mcp_backtest.ini" << INI
|
|
[Tester]
|
|
Expert=${EXPERT}
|
|
Symbol=${SYMBOL}
|
|
Period=M5
|
|
Deposit=${DEPOSIT}
|
|
Currency=${CURRENCY}
|
|
Leverage=${LEVERAGE}
|
|
Model=${MODEL}
|
|
FromDate=${FROM_DATE}
|
|
ToDate=${TO_DATE}
|
|
Report=C:\\mt5mcp_opt_report
|
|
Optimization=2
|
|
ExpertParameters=${EXPERT}.set
|
|
ShutdownTerminal=1
|
|
INI
|
|
|
|
cat > "${WINE_PREFIX_DIR}/drive_c/mt5mcp_run.bat" << 'EOF'
|
|
@echo off
|
|
"C:\Program Files\MetaTrader 5\terminal64.exe" /config:C:\mt5mcp_backtest.ini
|
|
EOF
|
|
|
|
# ── Count optimization combinations ──────────────────────────────────────────
|
|
COMBINATIONS=$(python3 - << PYEOF
|
|
import re, math
|
|
|
|
with open("${SET_FILE}", 'r', errors='replace') as f:
|
|
lines = f.readlines()
|
|
|
|
total = 1
|
|
for line in lines:
|
|
line = line.strip()
|
|
if line.startswith(';') or '=' not in line:
|
|
continue
|
|
# Format: param=value||start||step||stop||Y
|
|
parts = line.split('||')
|
|
if len(parts) >= 5 and parts[-1].strip().upper() == 'Y':
|
|
try:
|
|
start = float(parts[1])
|
|
step = float(parts[2])
|
|
stop = float(parts[3])
|
|
count = max(1, int((stop - start) / step) + 1)
|
|
total *= count
|
|
except (ValueError, ZeroDivisionError):
|
|
pass
|
|
|
|
print(total)
|
|
PYEOF
|
|
)
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo " MT5-Quant Genetic Optimization"
|
|
echo " Job ID: $JOB_ID"
|
|
echo " Expert: $EXPERT"
|
|
echo " Symbol: $SYMBOL Model: ${MODEL} (every tick)"
|
|
echo " Period: $FROM_DATE → $TO_DATE"
|
|
echo " Set file: $SET_FILE"
|
|
echo " Combos: $COMBINATIONS (genetic — converges in ~300-500 passes)"
|
|
echo " Log: $LOG_FILE"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
# ── Launch detached ───────────────────────────────────────────────────────────
|
|
# nohup: prevents SIGHUP when parent (Claude task, SSH session) exits
|
|
# disown: removes from shell job table so shell exit doesn't kill it
|
|
# Both are required for true detachment.
|
|
|
|
nohup bash -c "${MT5_ARCH} '${MT5_WINE}' cmd.exe /c 'C:\\mt5mcp_run.bat' 2>/dev/null || true" \
|
|
> "$LOG_FILE" 2>&1 &
|
|
OPT_PID=$!
|
|
disown $OPT_PID
|
|
|
|
# Write job metadata
|
|
JOBS_DIR="${ROOT_DIR}/.mt5mcp_jobs"
|
|
mkdir -p "$JOBS_DIR"
|
|
cat > "${JOBS_DIR}/${JOB_ID}.json" << JEOF
|
|
{
|
|
"job_id": "${JOB_ID}",
|
|
"pid": ${OPT_PID},
|
|
"expert": "${EXPERT}",
|
|
"symbol": "${SYMBOL}",
|
|
"from_date": "${FROM_DATE}",
|
|
"to_date": "${TO_DATE}",
|
|
"set_file": "${SET_FILE}",
|
|
"combinations": ${COMBINATIONS},
|
|
"log_file": "${LOG_FILE}",
|
|
"wine_prefix": "${WINE_PREFIX_DIR}",
|
|
"started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
}
|
|
JEOF
|
|
|
|
echo ""
|
|
echo " Launched (pid: $OPT_PID)"
|
|
echo " Optimization runs for 2-6 hours. Do NOT kill this process."
|
|
echo " Signal when MT5 shows 'Optimization complete' and use:"
|
|
echo " python3 analytics/optimize_parser.py --job $JOB_ID"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|