Files
mt5-quant/scripts/optimize.sh
T
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

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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"