feat: complete Rust migration with modular architecture
Major changes: - Migrate Python/shell scripts to Rust modules: - analytics/extract.py → src/analytics/extract.rs (ReportExtractor) - analytics/analyze.py → src/analytics/analyze.rs (DealAnalyzer) - scripts/mqlcompile.sh → src/compile/mql_compiler.rs (MqlCompiler) - scripts/backtest_pipeline.sh → src/pipeline/backtest.rs (BacktestPipeline) - New modular structure: - src/models/ - Config, Deal, Metrics, Report structs - src/analytics/ - Report parsing and deal analysis - src/compile/ - MQL5 compilation via Wine - src/pipeline/ - 5-stage backtest orchestration - src/tools/ - 27 MCP tool definitions and handlers - Remove PyInstaller setup (now pure Rust) - Remove migrated shell scripts (backtest_pipeline.sh, mqlcompile.sh) - Add GitHub Actions CI/CD for macOS & Linux releases - Update all documentation for Rust architecture Binary size: 4.3MB (no Python dependencies) Tools: 27 MCP tools fully functional
This commit is contained in:
@@ -1,530 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# backtest_pipeline.sh — 5-stage MT5 backtest pipeline
|
||||
# Stages: COMPILE → CLEAN → BACKTEST → EXTRACT → ANALYZE
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/backtest_pipeline.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --expert NAME EA name (without path or .mq5 extension)
|
||||
# --symbol SYMBOL Trading symbol (default: from config)
|
||||
# --from YYYY.MM.DD Start date
|
||||
# --to YYYY.MM.DD End date
|
||||
# --preset PRESET last_month | last_3months | ytd | last_year
|
||||
# --timeframe TF M1 M5 M15 M30 H1 H4 D1 (default: M5)
|
||||
# --deposit AMOUNT Initial deposit (default: from config)
|
||||
# --model 0|1|2 Tick model (default: 0=every tick)
|
||||
# --set FILE Path to .set parameter file
|
||||
# --leverage N Leverage (default: 500)
|
||||
# --skip-compile Skip compilation stage
|
||||
# --skip-clean Skip cache clean stage
|
||||
# --skip-analyze Skip analysis stage (extract only)
|
||||
# --deep Run deep analysis (hourly + volume profile)
|
||||
# --strategy NAME Analysis strategy profile: grid (default) | scalper | trend | hedge | generic
|
||||
# --timeout N Backtest timeout in seconds (default: 900)
|
||||
# --shutdown Close MT5 after backtest (default: keep open). Use for CI/headless.
|
||||
# --kill-existing Kill running MT5 before launching. Required when MT5 is open.
|
||||
# With default mode (no --shutdown): MT5 restarts, runs backtest,
|
||||
# then stays open so you can inspect results in the GUI.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Resolve real physical path (follows symlinks) so analytics/ is found even when
|
||||
# scripts/ is a symlink (e.g. ~/.config/mt5-quant/scripts -> /path/to/mt5-quant/scripts)
|
||||
REAL_SCRIPT_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${REAL_SCRIPT_DIR}/.." && pwd)"
|
||||
source "${SCRIPT_DIR}/platform_detect.sh"
|
||||
|
||||
# ── Defaults from config ──────────────────────────────────────────────────────
|
||||
DEFAULT_SYMBOL=$(_cfg "backtest_symbol" "XAUUSD")
|
||||
DEFAULT_DEPOSIT=$(_cfg "backtest_deposit" "10000")
|
||||
DEFAULT_CURRENCY=$(_cfg "backtest_currency" "USD")
|
||||
DEFAULT_LEVERAGE=$(_cfg "backtest_leverage" "500")
|
||||
DEFAULT_MODEL=$(_cfg "backtest_model" "0")
|
||||
DEFAULT_TF=$(_cfg "backtest_timeframe" "M5")
|
||||
DEFAULT_TIMEOUT=$(_cfg "backtest_timeout" "900")
|
||||
REPORTS_DIR="$(_cfg "reports_dir" "${ROOT_DIR}/reports")"
|
||||
# Optional: force headless terminal to a specific broker account (needed when live
|
||||
# trading terminal uses a different broker than the backtest symbol requires).
|
||||
DEFAULT_LOGIN=$(_cfg "backtest_login" "")
|
||||
DEFAULT_SERVER=$(_cfg "backtest_server" "")
|
||||
|
||||
# ── Parse arguments ───────────────────────────────────────────────────────────
|
||||
EXPERT=""
|
||||
SYMBOL="$DEFAULT_SYMBOL"
|
||||
FROM_DATE=""
|
||||
TO_DATE=""
|
||||
PRESET=""
|
||||
TIMEFRAME="$DEFAULT_TF"
|
||||
DEPOSIT="$DEFAULT_DEPOSIT"
|
||||
CURRENCY="$DEFAULT_CURRENCY"
|
||||
MODEL="$DEFAULT_MODEL"
|
||||
SET_FILE=""
|
||||
LEVERAGE="$DEFAULT_LEVERAGE"
|
||||
SKIP_COMPILE=false
|
||||
SKIP_CLEAN=false
|
||||
SKIP_ANALYZE=false
|
||||
DEEP_ANALYZE=false
|
||||
STRATEGY="grid"
|
||||
TIMEOUT="$DEFAULT_TIMEOUT"
|
||||
PROJECT_DIR="$(_cfg "project_dir" "")"
|
||||
GUI_MODE=false
|
||||
SHUTDOWN_TERMINAL=false
|
||||
KILL_EXISTING=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--expert) EXPERT="$2"; shift 2 ;;
|
||||
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--gui) GUI_MODE=true; shift ;;
|
||||
--symbol) SYMBOL="$2"; shift 2 ;;
|
||||
--from) FROM_DATE="$2"; shift 2 ;;
|
||||
--to) TO_DATE="$2"; shift 2 ;;
|
||||
--preset) PRESET="$2"; shift 2 ;;
|
||||
--timeframe) TIMEFRAME="$2"; shift 2 ;;
|
||||
--deposit) DEPOSIT="$2"; shift 2 ;;
|
||||
--model) MODEL="$2"; shift 2 ;;
|
||||
--set) SET_FILE="$2"; shift 2 ;;
|
||||
--leverage) LEVERAGE="$2"; shift 2 ;;
|
||||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||||
--skip-compile) SKIP_COMPILE=true; shift ;;
|
||||
--skip-clean) SKIP_CLEAN=true; shift ;;
|
||||
--skip-analyze) SKIP_ANALYZE=true; shift ;;
|
||||
--deep) DEEP_ANALYZE=true; shift ;;
|
||||
--strategy) STRATEGY="$2"; shift 2 ;;
|
||||
--shutdown) SHUTDOWN_TERMINAL=true; shift ;;
|
||||
--kill-existing) KILL_EXISTING=true; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$EXPERT" ]] && { echo "ERROR: --expert is required" >&2; exit 1; }
|
||||
|
||||
# ── Preset date resolution ────────────────────────────────────────────────────
|
||||
if [[ -n "$PRESET" ]]; then
|
||||
TODAY=$(date +%Y.%m.%d)
|
||||
case "$PRESET" in
|
||||
last_month)
|
||||
FROM_DATE=$(date -d "1 month ago" +%Y.%m.01 2>/dev/null || \
|
||||
date -v-1m +%Y.%m.01)
|
||||
TO_DATE="$TODAY"
|
||||
;;
|
||||
last_3months)
|
||||
FROM_DATE=$(date -d "3 months ago" +%Y.%m.01 2>/dev/null || \
|
||||
date -v-3m +%Y.%m.01)
|
||||
TO_DATE="$TODAY"
|
||||
;;
|
||||
ytd)
|
||||
FROM_DATE=$(date +%Y.01.01)
|
||||
TO_DATE="$TODAY"
|
||||
;;
|
||||
last_year)
|
||||
PREV_YEAR=$(( $(date +%Y) - 1 ))
|
||||
FROM_DATE="${PREV_YEAR}.01.01"
|
||||
TO_DATE="${PREV_YEAR}.12.31"
|
||||
;;
|
||||
*) echo "ERROR: Unknown preset: $PRESET" >&2; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
[[ -z "$FROM_DATE" || -z "$TO_DATE" ]] && {
|
||||
echo "ERROR: Provide --from/--to dates or --preset" >&2; exit 1
|
||||
}
|
||||
|
||||
# ── Report directory ──────────────────────────────────────────────────────────
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
REPORT_ID="${TIMESTAMP}_${EXPERT}_${SYMBOL}_${TIMEFRAME}"
|
||||
REPORT_DIR="${REPORTS_DIR}/${REPORT_ID}"
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
||||
PIPELINE_START=$(date +%s)
|
||||
PROGRESS_LOG="${REPORT_DIR}/progress.log"
|
||||
_progress() { echo "$1 $(date -u +%Y-%m-%dT%H:%M:%SZ) elapsed=$(( $(date +%s) - PIPELINE_START ))" >> "$PROGRESS_LOG"; }
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " MT5-Quant Backtest Pipeline"
|
||||
echo " Expert: $EXPERT"
|
||||
echo " Symbol: $SYMBOL Timeframe: $TIMEFRAME Model: $MODEL"
|
||||
echo " Period: $FROM_DATE → $TO_DATE"
|
||||
echo " Deposit: $CURRENCY $DEPOSIT Leverage: 1:$LEVERAGE"
|
||||
[[ -n "$SET_FILE" ]] && echo " Set file: $SET_FILE"
|
||||
echo " Report: $REPORT_DIR"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# ── Resolve platform ──────────────────────────────────────────────────────────
|
||||
resolve_platform
|
||||
|
||||
# ── Stage 1: COMPILE ──────────────────────────────────────────────────────────
|
||||
if [[ "$SKIP_COMPILE" == false ]]; then
|
||||
_progress "COMPILE"
|
||||
echo ""
|
||||
echo "[1/5] COMPILE"
|
||||
|
||||
# Find source file — check project.dir first, then fall back to pipeline root
|
||||
EA_SOURCE=""
|
||||
search_paths=(
|
||||
"${ROOT_DIR}/src/experts/${EXPERT}.mq5"
|
||||
"${ROOT_DIR}/src/${EXPERT}.mq5"
|
||||
"${ROOT_DIR}/${EXPERT}.mq5"
|
||||
)
|
||||
if [[ -n "$PROJECT_DIR" ]]; then
|
||||
search_paths=(
|
||||
"${PROJECT_DIR}/src/experts/${EXPERT}.mq5"
|
||||
"${PROJECT_DIR}/src/${EXPERT}.mq5"
|
||||
"${PROJECT_DIR}/${EXPERT}.mq5"
|
||||
"${search_paths[@]}"
|
||||
)
|
||||
fi
|
||||
for search_path in "${search_paths[@]}"; do
|
||||
[[ -f "$search_path" ]] && { EA_SOURCE="$search_path"; break; }
|
||||
done
|
||||
|
||||
[[ -z "$EA_SOURCE" ]] && {
|
||||
echo " ERROR: Cannot find ${EXPERT}.mq5" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
"${SCRIPT_DIR}/mqlcompile.sh" "$EA_SOURCE"
|
||||
else
|
||||
echo "[1/5] COMPILE skipped"
|
||||
fi
|
||||
|
||||
# ── Stage 2: CLEAN ────────────────────────────────────────────────────────────
|
||||
if [[ "$SKIP_CLEAN" == false ]]; then
|
||||
_progress "CLEAN"
|
||||
echo ""
|
||||
echo "[2/5] CLEAN"
|
||||
|
||||
# Clear tester cache
|
||||
if [[ -d "$MT5_CACHE_DIR" ]]; then
|
||||
find "$MT5_CACHE_DIR" -name "*.tst" -delete 2>/dev/null || true
|
||||
echo " Cleared tester cache: $MT5_CACHE_DIR"
|
||||
fi
|
||||
|
||||
# Remove cached .set file for this expert
|
||||
CACHED_SET="${MT5_TESTER_DIR}/${EXPERT}.set"
|
||||
if [[ -f "$CACHED_SET" ]]; then
|
||||
rm -f "$CACHED_SET"
|
||||
echo " Removed cached .set: $CACHED_SET"
|
||||
fi
|
||||
|
||||
# Reset terminal.ini OptMode — after any test/optimization MT5 sets OptMode=-1
|
||||
# which causes the next headless run to exit immediately (exit 49, no report)
|
||||
TERMINAL_INI="${MT5_DIR}/config/terminal.ini"
|
||||
if [[ -f "$TERMINAL_INI" ]]; then
|
||||
python3 -c "
|
||||
import sys, re
|
||||
path = sys.argv[1]
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
# Detect encoding: UTF-16 with BOM, or plain text
|
||||
if raw[:2] in (b'\xff\xfe', b'\xfe\xff'):
|
||||
text = raw.decode('utf-16')
|
||||
encoding = 'utf-16'
|
||||
else:
|
||||
text = raw.decode('utf-8', errors='replace')
|
||||
encoding = 'utf-8'
|
||||
text = re.sub(r'(?m)^OptMode=-1\s*$', 'OptMode=0', text)
|
||||
text = re.sub(r'(?m)^LastOptimization=1\s*\n?', '', text)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(text.encode(encoding))
|
||||
print(' Reset OptMode=-1 -> OptMode=0 in terminal.ini')
|
||||
except Exception as e:
|
||||
print(f' Warning: could not reset OptMode in terminal.ini: {e}')
|
||||
" "$TERMINAL_INI" 2>/dev/null || true
|
||||
echo " Reset terminal.ini OptMode"
|
||||
fi
|
||||
else
|
||||
echo "[2/5] CLEAN skipped"
|
||||
fi
|
||||
|
||||
# ── Prepare .set file ─────────────────────────────────────────────────────────
|
||||
if [[ -n "$SET_FILE" ]]; then
|
||||
# Resolve relative paths against PROJECT_DIR (fallback: script ROOT_DIR, then CWD)
|
||||
if [[ ! -f "$SET_FILE" ]]; then
|
||||
for base in "$PROJECT_DIR" "$ROOT_DIR" "$(pwd)"; do
|
||||
[[ -n "$base" && -f "${base}/${SET_FILE}" ]] && { SET_FILE="${base}/${SET_FILE}"; break; }
|
||||
done
|
||||
fi
|
||||
if [[ ! -f "$SET_FILE" ]]; then
|
||||
echo "ERROR: Set file not found: $SET_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Copy to tester profiles dir (MT5 reads from here)
|
||||
mkdir -p "$MT5_TESTER_DIR"
|
||||
cp "$SET_FILE" "${MT5_TESTER_DIR}/${EXPERT}.set"
|
||||
SET_FILENAME="$(basename "$SET_FILE")"
|
||||
fi
|
||||
|
||||
# ── Stage 3: BACKTEST ─────────────────────────────────────────────────────────
|
||||
_progress "BACKTEST"
|
||||
echo ""
|
||||
echo "[3/5] BACKTEST"
|
||||
|
||||
# Detect running MT5 instance (match terminal64.exe regardless of wine wrapper name)
|
||||
_mt5_is_running() { pgrep -f "terminal64\.exe" > /dev/null 2>&1; }
|
||||
|
||||
MT5_WAS_RUNNING=false
|
||||
_mt5_is_running && MT5_WAS_RUNNING=true
|
||||
|
||||
# --kill-existing: terminate running MT5 before launch (use with --shutdown for CI)
|
||||
if $KILL_EXISTING && $MT5_WAS_RUNNING; then
|
||||
echo " Stopping running MT5 (--kill-existing)..."
|
||||
pkill -TERM -f "terminal64\.exe" 2>/dev/null || true
|
||||
for _i in 1 2 3 4 5; do
|
||||
sleep 1
|
||||
_mt5_is_running || break
|
||||
done
|
||||
_mt5_is_running && { pkill -KILL -f "terminal64\.exe" 2>/dev/null || true; sleep 1; }
|
||||
echo " MT5 stopped."
|
||||
MT5_WAS_RUNNING=false
|
||||
fi
|
||||
|
||||
# Build backtest.ini
|
||||
REPORT_FILENAME="${REPORT_ID}.htm"
|
||||
# Relative path — MT5 resolves against its working dir (C:\Program Files\MetaTrader 5\reports\)
|
||||
WINE_REPORT_PATH="reports\\${REPORT_FILENAME}"
|
||||
|
||||
INI_HOST_PATH="${MT5_DIR}/backtest_config.ini"
|
||||
REPORTS_HOST_DIR="${MT5_DIR}/reports"
|
||||
mkdir -p "$REPORTS_HOST_DIR"
|
||||
|
||||
INI_CONTENT=""
|
||||
if [[ -n "$DEFAULT_LOGIN" && -n "$DEFAULT_SERVER" ]]; then
|
||||
INI_CONTENT="[Common]
|
||||
Login=${DEFAULT_LOGIN}
|
||||
Server=${DEFAULT_SERVER}
|
||||
|
||||
"
|
||||
fi
|
||||
|
||||
# ShutdownTerminal=0 (default): MT5 stays open; report detected via file watching.
|
||||
# ShutdownTerminal=1 (--shutdown): MT5 exits after backtest; process-wait mode.
|
||||
SHUTDOWN_VAL=0
|
||||
$SHUTDOWN_TERMINAL && SHUTDOWN_VAL=1
|
||||
|
||||
INI_CONTENT+="[Tester]
|
||||
Expert=${EXPERT}.ex5
|
||||
Symbol=${SYMBOL}
|
||||
Period=${TIMEFRAME}
|
||||
Optimization=0
|
||||
Model=${MODEL}
|
||||
FromDate=${FROM_DATE}
|
||||
ToDate=${TO_DATE}
|
||||
ForwardMode=0
|
||||
Deposit=${DEPOSIT}
|
||||
Currency=${CURRENCY}
|
||||
ProfitInPips=1
|
||||
Leverage=${LEVERAGE}
|
||||
ExecutionMode=10
|
||||
OptimizationCriterion=0
|
||||
Visual=$([[ "$GUI_MODE" == true ]] && echo 1 || echo 0)
|
||||
Report=${WINE_REPORT_PATH}
|
||||
ReplaceReport=1
|
||||
ShutdownTerminal=${SHUTDOWN_VAL}
|
||||
"
|
||||
[[ -n "$SET_FILE" ]] && INI_CONTENT+="ExpertParameters=${EXPERT}.set
|
||||
"
|
||||
|
||||
# MT5 requires UTF-16LE with BOM — plain UTF-8 is silently ignored
|
||||
printf "%s" "$INI_CONTENT" | iconv -f UTF-8 -t UTF-16LE > "${INI_HOST_PATH}.tmp"
|
||||
printf '\xff\xfe' | cat - "${INI_HOST_PATH}.tmp" > "${INI_HOST_PATH}"
|
||||
rm -f "${INI_HOST_PATH}.tmp"
|
||||
|
||||
# Set Wine prefix
|
||||
WINE_PREFIX_DIR=$(dirname "$(dirname "$(dirname "$MT5_DIR")")")
|
||||
export WINEPREFIX="$WINE_PREFIX_DIR"
|
||||
export WINEDEBUG="-all"
|
||||
|
||||
BACKTEST_START=$(date +%s)
|
||||
BAT_PATH="${WINE_PREFIX_DIR}/drive_c/_mt5mcp_run.bat"
|
||||
|
||||
if $SHUTDOWN_TERMINAL; then
|
||||
# ── Synchronous mode (--shutdown) ─────────────────────────────────────────
|
||||
# terminal64.exe is a single-instance app: if MT5 is running, a second
|
||||
# launch exits immediately with no report. Kill first to avoid this.
|
||||
if $MT5_WAS_RUNNING; then
|
||||
echo " WARNING: MT5 is running — stopping it (required for --shutdown mode)."
|
||||
pkill -TERM -f "terminal64\.exe" 2>/dev/null || true
|
||||
for _i in 1 2 3 4 5; do sleep 1; _mt5_is_running || break; done
|
||||
_mt5_is_running && { pkill -KILL -f "terminal64\.exe" 2>/dev/null || true; sleep 1; }
|
||||
echo " MT5 stopped."
|
||||
fi
|
||||
cat > "$BAT_PATH" << 'BATEOF'
|
||||
@echo off
|
||||
cd /d "C:\Program Files\MetaTrader 5"
|
||||
start /wait terminal64.exe /config:"C:\Program Files\MetaTrader 5\backtest_config.ini"
|
||||
BATEOF
|
||||
echo " Launching MT5 (timeout: ${TIMEOUT}s, shutdown mode)..."
|
||||
set +e
|
||||
timeout "${TIMEOUT}" ${MT5_ARCH} "${MT5_WINE}" cmd.exe /c 'C:\_mt5mcp_run.bat' 2>/dev/null
|
||||
WINE_EXIT=$?
|
||||
set -e
|
||||
rm -f "$BAT_PATH"
|
||||
BACKTEST_ELAPSED=$(( $(date +%s) - BACKTEST_START ))
|
||||
echo " MT5 completed in ${BACKTEST_ELAPSED}s (exit: ${WINE_EXIT})"
|
||||
sleep 2
|
||||
# Locate report
|
||||
MT5_REPORT=""
|
||||
for ext in ".htm" ".htm.xml" ".html"; do
|
||||
candidate="${REPORTS_HOST_DIR}/${REPORT_ID}${ext}"
|
||||
[[ -f "$candidate" ]] && { MT5_REPORT="$candidate"; break; }
|
||||
done
|
||||
[[ -z "$MT5_REPORT" ]] && \
|
||||
MT5_REPORT=$(find "${MT5_DIR}" -maxdepth 3 -name "*.htm" -newer "${INI_HOST_PATH}" 2>/dev/null | head -1)
|
||||
else
|
||||
# ── Background mode (default) ──────────────────────────────────────────────
|
||||
# MT5 uses a single-instance lock per Wine prefix. On Wine/CrossOver, a second
|
||||
# terminal64.exe instance exits immediately without forwarding the config to
|
||||
# the running instance (unlike native Windows behaviour).
|
||||
# When MT5 is running, we must kill it first — but with ShutdownTerminal=0
|
||||
# the new instance stays open after the backtest, so the user can inspect
|
||||
# results in the GUI without a permanent disruption.
|
||||
if $MT5_WAS_RUNNING; then
|
||||
echo "" >&2
|
||||
echo " ERROR: MetaTrader 5 is already running." >&2
|
||||
echo "" >&2
|
||||
echo " On Wine/CrossOver a second MT5 instance cannot pass a backtest config" >&2
|
||||
echo " to the running terminal — it exits immediately with no report." >&2
|
||||
echo "" >&2
|
||||
echo " Recommended fix:" >&2
|
||||
echo " Add --kill-existing to your command." >&2
|
||||
echo " MT5 will restart, run the backtest, then stay open (ShutdownTerminal=0)" >&2
|
||||
echo " so you can inspect the Strategy Tester results in the GUI." >&2
|
||||
echo "" >&2
|
||||
echo " Alternative: close MT5 manually and re-run the backtest." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Launching MT5 in background (ShutdownTerminal=0) ..."
|
||||
# `start` (no /wait): cmd.exe launches terminal64.exe and exits immediately.
|
||||
cat > "$BAT_PATH" << 'BATEOF'
|
||||
@echo off
|
||||
cd /d "C:\Program Files\MetaTrader 5"
|
||||
start terminal64.exe /config:"C:\Program Files\MetaTrader 5\backtest_config.ini"
|
||||
BATEOF
|
||||
nohup ${MT5_ARCH} "${MT5_WINE}" cmd.exe /c 'C:\_mt5mcp_run.bat' &>/dev/null &
|
||||
LAUNCHER_PID=$!
|
||||
disown "$LAUNCHER_PID" 2>/dev/null || true
|
||||
sleep 5
|
||||
rm -f "$BAT_PATH"
|
||||
|
||||
# ── Poll for report file ───────────────────────────────────────────────────
|
||||
# MT5 writes the report when Strategy Tester finishes, before any shutdown.
|
||||
REPORT_DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
||||
MT5_REPORT=""
|
||||
echo " Waiting for backtest report (timeout: ${TIMEOUT}s)..."
|
||||
while [[ -z "$MT5_REPORT" ]]; do
|
||||
if [[ $(date +%s) -ge $REPORT_DEADLINE ]]; then
|
||||
echo " ERROR: Timeout (${TIMEOUT}s) — no report produced." >&2
|
||||
echo " Possible causes:" >&2
|
||||
echo " - Symbol not found in MT5 history for this broker" >&2
|
||||
echo " - EA name mismatch (check .ex5 exists in Experts/)" >&2
|
||||
if $MT5_WAS_RUNNING; then
|
||||
echo " - Running MT5 ignored the config (try: --kill-existing)" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
ELAPSED=$(( $(date +%s) - BACKTEST_START ))
|
||||
for ext in ".htm" ".htm.xml" ".html"; do
|
||||
candidate="${REPORTS_HOST_DIR}/${REPORT_ID}${ext}"
|
||||
if [[ -f "$candidate" && -s "$candidate" ]]; then
|
||||
# Wait for file to stabilise (fully flushed to disk)
|
||||
S1=$(stat -f%z "$candidate" 2>/dev/null || stat -c%s "$candidate")
|
||||
sleep 2
|
||||
S2=$(stat -f%z "$candidate" 2>/dev/null || stat -c%s "$candidate")
|
||||
[[ "$S1" -gt 0 && "$S1" == "$S2" ]] && { MT5_REPORT="$candidate"; break 2; }
|
||||
fi
|
||||
done
|
||||
printf " ... %ds elapsed\r" "$ELAPSED"
|
||||
done
|
||||
BACKTEST_ELAPSED=$(( $(date +%s) - BACKTEST_START ))
|
||||
echo " Report ready in ${BACKTEST_ELAPSED}s"
|
||||
fi
|
||||
|
||||
# ── Locate report file ────────────────────────────────────────────────────────
|
||||
if [[ -z "$MT5_REPORT" ]]; then
|
||||
echo " ERROR: MT5 produced no report." >&2
|
||||
echo " Check: symbol name, date range, EA name, and that MT5 ran to completion." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Report: $MT5_REPORT"
|
||||
|
||||
# ── Stage 4: EXTRACT ─────────────────────────────────────────────────────────
|
||||
_progress "EXTRACT"
|
||||
echo ""
|
||||
echo "[4/5] EXTRACT"
|
||||
|
||||
python3 "${ROOT_DIR}/analytics/extract.py" \
|
||||
"$MT5_REPORT" \
|
||||
--output-dir "$REPORT_DIR" \
|
||||
&& echo " → metrics.json, deals.csv, deals.json"
|
||||
|
||||
# ── Stage 5: ANALYZE ─────────────────────────────────────────────────────────
|
||||
if [[ "$SKIP_ANALYZE" == false ]]; then
|
||||
_progress "ANALYZE"
|
||||
echo ""
|
||||
echo "[5/5] ANALYZE"
|
||||
|
||||
ANALYZE_FLAGS="$STRATEGY"
|
||||
[[ "$DEEP_ANALYZE" == true ]] && ANALYZE_FLAGS="$ANALYZE_FLAGS --deep"
|
||||
|
||||
python3 "${ROOT_DIR}/analytics/analyze.py" \
|
||||
$ANALYZE_FLAGS \
|
||||
"${REPORT_DIR}/deals.csv" \
|
||||
--output-dir "$REPORT_DIR" \
|
||||
&& echo " → analysis.json [$STRATEGY]"
|
||||
else
|
||||
echo "[5/5] ANALYZE skipped"
|
||||
fi
|
||||
|
||||
# ── Save pipeline metadata ────────────────────────────────────────────────────
|
||||
_progress "DONE"
|
||||
PIPELINE_ELAPSED=$(( $(date +%s) - PIPELINE_START ))
|
||||
|
||||
python3 - << PYEOF
|
||||
import json, os
|
||||
meta = {
|
||||
"expert": "${EXPERT}",
|
||||
"symbol": "${SYMBOL}",
|
||||
"timeframe": "${TIMEFRAME}",
|
||||
"from_date": "${FROM_DATE}",
|
||||
"to_date": "${TO_DATE}",
|
||||
"deposit": ${DEPOSIT},
|
||||
"currency": "${CURRENCY}",
|
||||
"model": ${MODEL},
|
||||
"leverage": ${LEVERAGE},
|
||||
"set_file": "${SET_FILE}",
|
||||
"report_dir": "${REPORT_DIR}",
|
||||
"duration_seconds": ${PIPELINE_ELAPSED},
|
||||
"files": {
|
||||
"metrics": "${REPORT_DIR}/metrics.json",
|
||||
"analysis": "${REPORT_DIR}/analysis.json",
|
||||
"deals_csv": "${REPORT_DIR}/deals.csv",
|
||||
"deals_json": "${REPORT_DIR}/deals.json"
|
||||
}
|
||||
}
|
||||
with open("${REPORT_DIR}/pipeline_metadata.json", "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
PYEOF
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Pipeline complete in ${PIPELINE_ELAPSED}s"
|
||||
echo " Report: $REPORT_DIR"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Print key metrics inline
|
||||
if [[ -f "${REPORT_DIR}/metrics.json" ]]; then
|
||||
python3 - << PYEOF
|
||||
import json
|
||||
with open("${REPORT_DIR}/metrics.json") as f:
|
||||
m = json.load(f)
|
||||
print(f" Profit: \${m.get('net_profit',0):,.2f} PF: {m.get('profit_factor',0):.2f} DD: {m.get('max_dd_pct',0):.2f}% Sharpe: {m.get('sharpe_ratio',0):.2f} Trades: {m.get('total_trades',0)}")
|
||||
PYEOF
|
||||
fi
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build MT5-Quant as a directory bundle (onedir mode) using PyInstaller
|
||||
# This mode preserves stdin/stdout better for MCP communication
|
||||
# Output: dist/mt5-quant/ directory
|
||||
|
||||
set -e
|
||||
|
||||
# Get script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== MT5-Quant PyInstaller Build (ONEDIR Mode) ==="
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
echo ""
|
||||
|
||||
# Determine Python interpreter
|
||||
if [ -f "$PROJECT_ROOT/.venv/bin/python3" ]; then
|
||||
PYTHON="$PROJECT_ROOT/.venv/bin/python3"
|
||||
elif [ -f "$PROJECT_ROOT/.venv/bin/python" ]; then
|
||||
PYTHON="$PROJECT_ROOT/.venv/bin/python"
|
||||
else
|
||||
PYTHON="python3"
|
||||
fi
|
||||
|
||||
echo "Using Python: $PYTHON"
|
||||
echo ""
|
||||
|
||||
# Ensure dependencies are installed
|
||||
echo "Installing dependencies..."
|
||||
$PYTHON -m pip install pyinstaller mcp pyyaml typer --quiet 2>/dev/null || true
|
||||
|
||||
# Clean previous builds
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf "$PROJECT_ROOT/build" "$PROJECT_ROOT/dist"
|
||||
|
||||
# Build the executable in onedir mode
|
||||
echo "Building executable in ONEDIR mode (this may take 1-2 minutes)..."
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Use onedir mode (no --onefile flag)
|
||||
# Note: mode is controlled by the .spec file
|
||||
$PYTHON -m PyInstaller \
|
||||
--clean \
|
||||
--noconfirm \
|
||||
--distpath "$PROJECT_ROOT/dist" \
|
||||
--workpath "$PROJECT_ROOT/build" \
|
||||
"$PROJECT_ROOT/mt5-quant.spec"
|
||||
|
||||
# Report results
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo ""
|
||||
echo "Executable location:"
|
||||
ls -la "$PROJECT_ROOT/dist/mt5-quant/" | head -10
|
||||
|
||||
echo ""
|
||||
echo "Main executable:"
|
||||
ls -lh "$PROJECT_ROOT/dist/mt5-quant/mt5-quant"
|
||||
|
||||
echo ""
|
||||
echo "Total size:"
|
||||
du -sh "$PROJECT_ROOT/dist/mt5-quant/"
|
||||
|
||||
echo ""
|
||||
echo "To test:"
|
||||
echo " ./dist/mt5-quant/mt5-quant"
|
||||
echo ""
|
||||
echo "To register with Claude Code:"
|
||||
echo " claude mcp add MT5-Quant -- $(pwd)/dist/mt5-quant/mt5-quant"
|
||||
echo ""
|
||||
echo "=== Deployment to Multiple Machines ==="
|
||||
echo ""
|
||||
echo "1. Copy entire directory to target machines:"
|
||||
echo " scp -r dist/mt5-quant/ user@server:/opt/"
|
||||
echo " ssh user@server ln -s /opt/mt5-quant/mt5-quant /usr/local/bin/"
|
||||
echo ""
|
||||
echo "2. Or create tarball for distribution:"
|
||||
echo " tar -czf mt5-quant-macos-arm64.tar.gz -C dist mt5-quant"
|
||||
echo ""
|
||||
echo "Requirements on target machine:"
|
||||
echo " - MetaTrader 5 installed (via Wine on macOS/Linux)"
|
||||
echo " - Config file at ~/.config/mt5-quant/config/mt5-quant.yaml"
|
||||
echo " - NO Python installation required!"
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build MT5-Quant as a single executable using PyInstaller
|
||||
# Output: dist/mt5-quant
|
||||
|
||||
set -e
|
||||
|
||||
# Get script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== MT5-Quant PyInstaller Build ==="
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
echo ""
|
||||
|
||||
# Determine Python interpreter
|
||||
if [ -f "$PROJECT_ROOT/.venv/bin/python3" ]; then
|
||||
PYTHON="$PROJECT_ROOT/.venv/bin/python3"
|
||||
elif [ -f "$PROJECT_ROOT/.venv/bin/python" ]; then
|
||||
PYTHON="$PROJECT_ROOT/.venv/bin/python"
|
||||
else
|
||||
PYTHON="python3"
|
||||
fi
|
||||
|
||||
echo "Using Python: $PYTHON"
|
||||
echo ""
|
||||
|
||||
# Ensure dependencies are installed
|
||||
echo "Installing dependencies..."
|
||||
$PYTHON -m pip install pyinstaller mcp pyyaml --quiet
|
||||
|
||||
# Clean previous builds
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf "$PROJECT_ROOT/build" "$PROJECT_ROOT/dist"
|
||||
|
||||
# Build the executable
|
||||
echo "Building executable (this may take 1-2 minutes)..."
|
||||
cd "$PROJECT_ROOT"
|
||||
$PYTHON -m PyInstaller \
|
||||
--clean \
|
||||
--noconfirm \
|
||||
--distpath "$PROJECT_ROOT/dist" \
|
||||
--workpath "$PROJECT_ROOT/build" \
|
||||
"$PROJECT_ROOT/mt5-quant.spec"
|
||||
|
||||
# Report results
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo ""
|
||||
echo "Executable location:"
|
||||
ls -lh dist/mt5-quant
|
||||
|
||||
echo ""
|
||||
echo "Size breakdown:"
|
||||
du -sh dist/ 2>/dev/null || echo " dist/: $(du -sh dist/mt5-quant 2>/dev/null | cut -f1)"
|
||||
|
||||
echo ""
|
||||
echo "To test:"
|
||||
echo " ./dist/mt5-quant --help"
|
||||
echo ""
|
||||
echo "To register with Claude Code:"
|
||||
echo " claude mcp add MT5-Quant -- $(pwd)/dist/mt5-quant"
|
||||
echo ""
|
||||
echo "=== Deployment to Multiple Machines ==="
|
||||
echo ""
|
||||
echo "1. Copy this single binary to target machines:"
|
||||
echo " scp dist/mt5-quant user@server:/usr/local/bin/"
|
||||
echo " ssh user@server chmod +x /usr/local/bin/mt5-quant"
|
||||
echo ""
|
||||
echo "2. Copy config directory (includes mt5-quant.yaml):"
|
||||
echo " scp -r config/ user@server:~/.config/mt5-quant/"
|
||||
echo ""
|
||||
echo "3. Or use bundled config (minimal):"
|
||||
echo " MT5_MCP_HOME=/path/to/config ./mt5-quant"
|
||||
echo ""
|
||||
echo "4. Register on target machine:"
|
||||
echo " claude mcp add MT5-Quant -- /usr/local/bin/mt5-quant"
|
||||
echo ""
|
||||
echo "Requirements on target machine:"
|
||||
echo " - MetaTrader 5 installed (via Wine on macOS/Linux)"
|
||||
echo " - Config file at ~/.config/mt5-quant/config/mt5-quant.yaml"
|
||||
echo " - NO Python installation required!"
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Build release binaries for distribution
|
||||
# Creates: dist/mt5-quant-{platform}.tar.gz
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
VERSION=$(grep -E '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
echo "=== Building MT5-Quant v${VERSION} ==="
|
||||
echo ""
|
||||
|
||||
# Clean previous builds
|
||||
rm -rf "$PROJECT_ROOT/dist"
|
||||
mkdir -p "$PROJECT_ROOT/dist"
|
||||
|
||||
# Build current platform
|
||||
echo "Building for current platform..."
|
||||
cargo build --release
|
||||
|
||||
# Detect platform
|
||||
UNAME=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
|
||||
if [[ "$UNAME" == "Darwin" ]]; then
|
||||
PLATFORM="macos-${ARCH}"
|
||||
elif [[ "$UNAME" == "Linux" ]]; then
|
||||
PLATFORM="linux-${ARCH}"
|
||||
else
|
||||
PLATFORM="unknown"
|
||||
fi
|
||||
|
||||
PACKAGE_NAME="mt5-quant-${PLATFORM}"
|
||||
PACKAGE_DIR="$PROJECT_ROOT/dist/${PACKAGE_NAME}"
|
||||
|
||||
echo "Packaging for ${PLATFORM}..."
|
||||
mkdir -p "$PACKAGE_DIR"
|
||||
|
||||
# Copy binary
|
||||
cp "$PROJECT_ROOT/target/release/mt5-quant" "$PACKAGE_DIR/"
|
||||
|
||||
# Copy config template
|
||||
mkdir -p "$PACKAGE_DIR/config"
|
||||
cp "$PROJECT_ROOT/config/mt5-quant.example.yaml" "$PACKAGE_DIR/config/"
|
||||
|
||||
# Copy docs
|
||||
cp "$PROJECT_ROOT/README.md" "$PACKAGE_DIR/"
|
||||
cp "$PROJECT_ROOT/WINDSURF_SETUP.md" "$PACKAGE_DIR/"
|
||||
cp "$PROJECT_ROOT/CLAUDE.md" "$PACKAGE_DIR/"
|
||||
|
||||
# Create tarball
|
||||
cd "$PROJECT_ROOT/dist"
|
||||
tar -czf "${PACKAGE_NAME}.tar.gz" "$PACKAGE_NAME"
|
||||
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo ""
|
||||
echo "Package: dist/${PACKAGE_NAME}.tar.gz"
|
||||
echo "Size: $(du -h "${PACKAGE_NAME}.tar.gz" | cut -f1)"
|
||||
echo ""
|
||||
echo "Contents:"
|
||||
tar -tzf "${PACKAGE_NAME}.tar.gz" | head -10
|
||||
echo ""
|
||||
echo "To install:"
|
||||
echo " tar -xzf ${PACKAGE_NAME}.tar.gz"
|
||||
echo " cd ${PACKAGE_NAME}"
|
||||
echo " ./mt5-quant --help"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Build MT5-Quant Rust MCP Server
|
||||
# Output: target/release/mt5-quant
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "=== MT5-Quant Rust Build ==="
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
echo ""
|
||||
|
||||
echo "Building release binary..."
|
||||
cargo build --release
|
||||
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo ""
|
||||
echo "Executable location:"
|
||||
ls -lh "$PROJECT_ROOT/target/release/mt5-quant"
|
||||
|
||||
echo ""
|
||||
echo "To test:"
|
||||
echo " ./target/release/mt5-quant --help"
|
||||
echo ""
|
||||
echo "To install for Windsurf:"
|
||||
echo " Update ~/.windsurf/config.yaml:"
|
||||
echo " command: $PROJECT_ROOT/target/release/mt5-quant"
|
||||
echo ""
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# mqlcompile.sh — Compile an MQL5 Expert Advisor via MetaEditor (Wine/CrossOver)
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/mqlcompile.sh <path/to/Expert.mq5>
|
||||
#
|
||||
# Output:
|
||||
# Compiled .ex5 written to MT5_EXPERTS_DIR
|
||||
# Exit 0 on success, 1 on compile errors
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/platform_detect.sh"
|
||||
|
||||
# ── Args ──────────────────────────────────────────────────────────────────────
|
||||
SOURCE_FILE="${1:-}"
|
||||
if [[ -z "$SOURCE_FILE" ]]; then
|
||||
echo "Usage: $0 <path/to/Expert.mq5>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SOURCE_FILE" ]]; then
|
||||
echo "ERROR: Source file not found: $SOURCE_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_FILE="$(realpath "$SOURCE_FILE")"
|
||||
EXPERT_NAME="$(basename "$SOURCE_FILE" .mq5)"
|
||||
|
||||
# ── Resolve platform ──────────────────────────────────────────────────────────
|
||||
resolve_platform
|
||||
|
||||
METAEDITOR="${MT5_DIR}/metaeditor64.exe"
|
||||
if [[ ! -f "$METAEDITOR" ]]; then
|
||||
echo "ERROR: metaeditor64.exe not found at: $METAEDITOR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Copy source to MT5 Experts source dir ────────────────────────────────────
|
||||
# MetaEditor requires the source file to be inside the MT5 directory tree
|
||||
MT5_SRC_DIR="${MT5_DIR}/MQL5/Experts"
|
||||
mkdir -p "$MT5_SRC_DIR"
|
||||
cp "$SOURCE_FILE" "${MT5_SRC_DIR}/${EXPERT_NAME}.mq5"
|
||||
|
||||
WINE_SRC_PATH="$(host_to_wine_path "${MT5_SRC_DIR}/${EXPERT_NAME}.mq5")"
|
||||
|
||||
# ── Sync .mqh include files to MT5 Include dir ───────────────────────────────
|
||||
# Auto-detect include/ directory relative to source file. Searches up to 2
|
||||
# levels above the source file for an include/ sibling directory.
|
||||
# Layout supported:
|
||||
# <project>/experts/EA.mq5 + <project>/include/<subdir>/*.mqh
|
||||
# <project>/src/experts/EA.mq5 + <project>/src/include/<subdir>/*.mqh
|
||||
_find_include_dir() {
|
||||
local source_dir="$1"
|
||||
local candidate
|
||||
for candidate in "$source_dir" "$(dirname "$source_dir")" "$(dirname "$(dirname "$source_dir")")"; do
|
||||
if [[ -d "${candidate}/include" ]]; then
|
||||
echo "${candidate}/include"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
INCLUDE_BASE=""
|
||||
INCLUDE_BASE=$(_find_include_dir "$(dirname "$SOURCE_FILE")") || true
|
||||
|
||||
if [[ -n "$INCLUDE_BASE" ]]; then
|
||||
synced_total=0
|
||||
# Sync each subdirectory under include/ → MQL5/Include/<subdir>/
|
||||
while IFS= read -r -d '' subdir; do
|
||||
dir_name="$(basename "$subdir")"
|
||||
mt5_dest="${MT5_DIR}/MQL5/Include/${dir_name}"
|
||||
rm -rf "$mt5_dest"
|
||||
cp -r "$subdir" "$mt5_dest"
|
||||
count=$(find "$mt5_dest" -name "*.mqh" | wc -l | tr -d ' ')
|
||||
echo "[compile] Synced ${count} .mqh → Include/${dir_name}/"
|
||||
synced_total=$((synced_total + count))
|
||||
done < <(find "$INCLUDE_BASE" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
|
||||
|
||||
# Also sync any .mqh files directly in include/ (flat layout)
|
||||
while IFS= read -r -d '' mqh; do
|
||||
cp "$mqh" "${MT5_DIR}/MQL5/Include/"
|
||||
synced_total=$((synced_total + 1))
|
||||
done < <(find "$INCLUDE_BASE" -maxdepth 1 -name "*.mqh" -print0 2>/dev/null)
|
||||
|
||||
if [[ $synced_total -eq 0 ]]; then
|
||||
echo "[compile] INFO: include/ found but contains no .mqh files — skipping sync"
|
||||
else
|
||||
echo "[compile] Synced ${synced_total} .mqh file(s) total"
|
||||
fi
|
||||
else
|
||||
echo "[compile] INFO: No include/ directory found — skipping .mqh sync"
|
||||
fi
|
||||
|
||||
# ── Set Wine prefix ───────────────────────────────────────────────────────────
|
||||
WINE_PREFIX_DIR=$(dirname "$(dirname "$(dirname "$MT5_DIR")")")
|
||||
export WINEPREFIX="$WINE_PREFIX_DIR"
|
||||
export WINEDEBUG="-all"
|
||||
|
||||
# ── Run MetaEditor ────────────────────────────────────────────────────────────
|
||||
echo "[compile] Compiling ${EXPERT_NAME}.mq5 ..."
|
||||
LOG_FILE="$(mktemp /tmp/mqlcompile_XXXXXX.log)"
|
||||
|
||||
set +e
|
||||
${MT5_ARCH} "${MT5_WINE}" "${METAEDITOR}" \
|
||||
/compile:"${WINE_SRC_PATH}" \
|
||||
/log:"${LOG_FILE}" \
|
||||
2>/dev/null
|
||||
WINE_EXIT=$?
|
||||
set -e
|
||||
|
||||
# MetaEditor always exits 0 on macOS/Wine; check log for errors
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
if [[ -f "$LOG_FILE" ]]; then
|
||||
# Log may be UTF-16LE
|
||||
LOG_TEXT=$(iconv -f UTF-16LE -t UTF-8 "$LOG_FILE" 2>/dev/null || cat "$LOG_FILE")
|
||||
ERRORS=$(echo "$LOG_TEXT" | grep -cE "^.*error" || true)
|
||||
WARNINGS=$(echo "$LOG_TEXT" | grep -cE "^.*warning" || true)
|
||||
|
||||
if [[ $ERRORS -gt 0 ]]; then
|
||||
echo "[compile] FAILED: $ERRORS error(s), $WARNINGS warning(s)"
|
||||
echo "$LOG_TEXT" | grep -E "error|warning" | head -20
|
||||
rm -f "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Verify .ex5 was produced ──────────────────────────────────────────────────
|
||||
EX5_PATH="${MT5_SRC_DIR}/${EXPERT_NAME}.ex5"
|
||||
if [[ ! -f "$EX5_PATH" ]]; then
|
||||
echo "[compile] ERROR: .ex5 not produced. MetaEditor may have failed silently." >&2
|
||||
[[ -f "$LOG_FILE" ]] && cat "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_SIZE=$(stat -f%z "$EX5_PATH" 2>/dev/null || stat -c%s "$EX5_PATH")
|
||||
echo "[compile] OK: ${EXPERT_NAME}.ex5 (${BINARY_SIZE} bytes, ${WARNINGS} warning(s))"
|
||||
|
||||
rm -f "$LOG_FILE"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user