feat: add Wine/MT5 debugging tools and update release workflow

- Add 9 debugging/diagnostics tools for Wine/MT5 crash investigation
- Update server.json with v1.30.0 and MCP package config
- Update README.md with 85 tools count and debugging section
- Update docs/MCP_TOOLS.md documentation
- Enhance release workflow with MCP packaging job
- Clean up mcp-package directory (now built in CI)
This commit is contained in:
Devid HW
2026-04-22 04:41:41 +07:00
parent 9be1296916
commit 0bc410f613
31 changed files with 634 additions and 3954 deletions
+81 -7
View File
@@ -3,7 +3,85 @@ use serde_json::{json, Value};
pub fn tool_run_backtest() -> Value {
json!({
"name": "run_backtest",
"description": "Run a complete MT5 backtest pipeline: compile → clean cache → backtest → extract → analyze",
"description": "Full backtest pipeline: compile EA → clean cache → run backtest → extract results → analyze. Use this when you have modified the EA source code.",
"inputSchema": {
"type": "object",
"required": ["expert"],
"properties": {
"expert": { "type": "string", "description": "EA name without path or extension" },
"symbol": { "type": "string", "description": "Trading symbol (default: from config or first available)" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD (default: past complete month)" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD (default: past complete month)" },
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1"], "description": "Chart timeframe (default: M5)" },
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" },
"model": { "type": "integer", "enum": [0, 1, 2], "description": "Tick model: 0=Every tick, 1=OHLC, 2=Open prices" },
"set_file": { "type": "string", "description": "Path to .set parameter file for EA inputs" },
"skip_compile": { "type": "boolean", "description": "Skip compilation (use existing .ex5)" },
"skip_clean": { "type": "boolean", "description": "Skip cache cleaning" },
"skip_analyze": { "type": "boolean", "description": "Skip analysis phase" },
"deep": { "type": "boolean", "description": "Run deep analysis with extra metrics" },
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest completes" },
"kill_existing": { "type": "boolean", "description": "Kill any running MT5 instance first" },
"timeout": { "type": "integer", "description": "Max wait time in seconds (default: 900)" },
"gui": { "type": "boolean", "description": "Enable MT5 visualization window" }
}
}
})
}
pub fn tool_run_backtest_quick() -> Value {
json!({
"name": "run_backtest_quick",
"description": "Quick backtest using pre-compiled EA: clean cache → run backtest → extract → analyze. Skips compilation. Use when EA code hasn't changed.",
"inputSchema": {
"type": "object",
"required": ["expert"],
"properties": {
"expert": { "type": "string", "description": "EA name without path or extension (must have .ex5 compiled)" },
"symbol": { "type": "string", "description": "Trading symbol (default: from config or first available)" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD (default: past complete month)" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD (default: past complete month)" },
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1"], "description": "Chart timeframe (default: M5)" },
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" },
"model": { "type": "integer", "enum": [0, 1, 2], "description": "Tick model" },
"set_file": { "type": "string", "description": "Path to .set parameter file for EA inputs" },
"deep": { "type": "boolean", "description": "Run deep analysis" },
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
"timeout": { "type": "integer", "description": "Max wait time in seconds (default: 900)" },
"gui": { "type": "boolean", "description": "Enable MT5 visualization" }
}
}
})
}
pub fn tool_run_backtest_only() -> Value {
json!({
"name": "run_backtest_only",
"description": "Backtest only: just run backtest and extract results. No compile, no analysis. Fastest option when you just need raw trade data.",
"inputSchema": {
"type": "object",
"required": ["expert"],
"properties": {
"expert": { "type": "string", "description": "EA name without path or extension (must have .ex5 compiled)" },
"symbol": { "type": "string", "description": "Trading symbol (default: from config)" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD" },
"timeframe": { "type": "string", "enum": ["M1", "M5", "M15", "M30", "H1", "H4", "D1"], "description": "Chart timeframe (default: M5)" },
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" },
"model": { "type": "integer", "enum": [0, 1, 2], "description": "Tick model" },
"set_file": { "type": "string", "description": "Path to .set parameter file" },
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
"timeout": { "type": "integer", "description": "Max wait time (default: 900)" },
"gui": { "type": "boolean", "description": "Enable MT5 visualization" }
}
}
})
}
pub fn tool_launch_backtest() -> Value {
json!({
"name": "launch_backtest",
"description": "Launch MT5 backtest in fire-and-forget mode: compile → clean cache → launch MT5 backtest, then return immediately. Use get_backtest_status to poll for completion.",
"inputSchema": {
"type": "object",
"required": ["expert"],
@@ -18,12 +96,8 @@ pub fn tool_run_backtest() -> Value {
"set_file": { "type": "string", "description": "Path to .set parameter file" },
"skip_compile": { "type": "boolean" },
"skip_clean": { "type": "boolean" },
"skip_analyze": { "type": "boolean" },
"deep": { "type": "boolean", "description": "Run deep analysis" },
"shutdown": { "type": "boolean", "description": "Close MT5 after backtest" },
"kill_existing": { "type": "boolean" },
"timeout": { "type": "integer" },
"gui": { "type": "boolean" }
"timeout": { "type": "integer", "description": "Max time in seconds to wait for backtest (default: 900)" },
"gui": { "type": "boolean", "description": "Enable visualization during backtest" }
}
}
})
+6 -3
View File
@@ -12,9 +12,12 @@ pub mod utility;
pub fn get_tools_list() -> Value {
let tools = vec![
// Backtest
backtest::tool_run_backtest(),
backtest::tool_get_backtest_status(),
// Backtest - Granular options
backtest::tool_run_backtest(), // Full pipeline: compile + clean + backtest + extract + analyze
backtest::tool_run_backtest_quick(), // Quick: skip compile, do clean + backtest + extract + analyze
backtest::tool_run_backtest_only(), // Minimal: skip compile, do clean + backtest + extract only
backtest::tool_launch_backtest(), // Fire-and-forget: compile + clean + launch MT5
backtest::tool_get_backtest_status(), // Poll for completion
backtest::tool_cache_status(),
backtest::tool_clean_cache(),
// Optimization
+15 -15
View File
@@ -3,17 +3,17 @@ use serde_json::{json, Value};
pub fn tool_run_optimization() -> Value {
json!({
"name": "run_optimization",
"description": "Launch MT5 genetic parameter optimization",
"description": "Launch MT5 genetic parameter optimization in fire-and-forget mode. Returns immediately with job_id. Use get_optimization_status to poll for completion. Optimization typically runs for 2-6 hours.",
"inputSchema": {
"type": "object",
"required": ["expert", "set_file", "from_date", "to_date"],
"properties": {
"expert": { "type": "string" },
"set_file": { "type": "string" },
"symbol": { "type": "string" },
"from_date": { "type": "string" },
"to_date": { "type": "string" },
"deposit": { "type": "integer" }
"expert": { "type": "string", "description": "EA name without path or extension" },
"set_file": { "type": "string", "description": "Path to .set file with parameter ranges for optimization" },
"symbol": { "type": "string", "description": "Trading symbol (default: XAUUSD)" },
"from_date": { "type": "string", "description": "Start date YYYY.MM.DD" },
"to_date": { "type": "string", "description": "End date YYYY.MM.DD" },
"deposit": { "type": "integer", "description": "Initial deposit (default: 10000)" }
}
}
})
@@ -22,12 +22,12 @@ pub fn tool_run_optimization() -> Value {
pub fn tool_get_optimization_status() -> Value {
json!({
"name": "get_optimization_status",
"description": "Check progress of a running optimization job",
"description": "Check progress of a running optimization job. Poll periodically until status shows 'completed'.",
"inputSchema": {
"type": "object",
"required": ["job_id"],
"properties": {
"job_id": { "type": "string" }
"job_id": { "type": "string", "description": "Job ID returned by run_optimization" }
}
}
})
@@ -36,14 +36,14 @@ pub fn tool_get_optimization_status() -> Value {
pub fn tool_get_optimization_results() -> Value {
json!({
"name": "get_optimization_results",
"description": "Parse completed MT5 optimization results",
"description": "Parse completed MT5 optimization results and find best parameter combinations",
"inputSchema": {
"type": "object",
"properties": {
"job_id": { "type": "string" },
"report_file": { "type": "string" },
"dd_threshold": { "type": "number" },
"top_n": { "type": "integer" }
"job_id": { "type": "string", "description": "Job ID to parse results for" },
"report_file": { "type": "string", "description": "Direct path to optimization report XML file" },
"dd_threshold": { "type": "number", "description": "Max drawdown percentage filter" },
"top_n": { "type": "integer", "description": "Number of top passes to return (default: 30)" }
}
}
})
@@ -52,7 +52,7 @@ pub fn tool_get_optimization_results() -> Value {
pub fn tool_list_jobs() -> Value {
json!({
"name": "list_jobs",
"description": "List running and completed optimization jobs",
"description": "List all running and completed optimization jobs with their status",
"inputSchema": {
"type": "object"
}
+217 -10
View File
@@ -2,7 +2,9 @@ use anyhow::Result;
use serde_json::{json, Value};
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::models::Config;
use crate::models::report::BacktestJob;
use crate::pipeline::backtest::{BacktestParams, BacktestPipeline};
/// Pre-flight check result for backtest readiness
@@ -190,38 +192,243 @@ pub async fn handle_run_backtest(config: &Config, args: &Value) -> Result<Value>
}))
}
pub async fn handle_run_backtest_quick(config: &Config, args: &Value) -> Result<Value> {
// Quick backtest: skip compile, do clean → backtest → extract → analyze
let mut args = args.clone();
if let Some(obj) = args.as_object_mut() {
obj.insert("skip_compile".to_string(), json!(true));
// keep skip_analyze as false (default) to run analysis
}
handle_run_backtest(config, &args).await
}
pub async fn handle_run_backtest_only(config: &Config, args: &Value) -> Result<Value> {
// Backtest only: skip compile, skip analyze - just backtest and extract
let mut args = args.clone();
if let Some(obj) = args.as_object_mut() {
obj.insert("skip_compile".to_string(), json!(true));
obj.insert("skip_analyze".to_string(), json!(true));
}
handle_run_backtest(config, &args).await
}
pub async fn handle_launch_backtest(config: &Config, args: &Value) -> Result<Value> {
let expert = args.get("expert")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("expert is required"))?;
// Run pre-flight check
let preflight = BacktestPreflight::check(config, expert);
// Check account session
if preflight.account.is_none() {
return Ok(json!({
"content": [{ "type": "text", "text": json!({
"error": "No active MT5 account session detected.",
"hint": "Open MT5 and login to your trading account before running backtests."
}).to_string() }],
"isError": true
}));
}
// Get symbol
let requested_symbol = args.get("symbol")
.and_then(|v| v.as_str())
.unwrap_or("");
let symbol = if requested_symbol.is_empty() {
config.backtest_symbol.clone()
.or_else(|| preflight.available_symbols.first().cloned())
.unwrap_or_else(|| "EURUSD".to_string())
} else {
requested_symbol.to_string()
};
// EA existence check
if !preflight.ea_exists {
return Ok(json!({
"content": [{ "type": "text", "text": json!({
"error": format!("EA '{}' not found in Experts directory.", expert),
"hint": "Use search_experts or list_experts to find available EAs."
}).to_string() }],
"isError": true
}));
}
// Date defaulting
let (from_date, to_date) = {
let f = args.get("from_date").and_then(|v| v.as_str()).unwrap_or("");
let t = args.get("to_date").and_then(|v| v.as_str()).unwrap_or("");
if f.is_empty() || t.is_empty() {
super::past_complete_month()
} else {
(f.to_string(), t.to_string())
}
};
let params = BacktestParams {
expert: expert.to_string(),
symbol: symbol.to_string(),
from_date: from_date.to_string(),
to_date: to_date.to_string(),
timeframe: args.get("timeframe").and_then(|v| v.as_str()).unwrap_or("M5").to_string(),
deposit: args.get("deposit").and_then(|v| v.as_u64()).unwrap_or(10000) as u32,
model: args.get("model").and_then(|v| v.as_u64()).unwrap_or(0) as u8,
leverage: args.get("leverage").and_then(|v| v.as_u64()).unwrap_or(500) as u32,
set_file: args.get("set_file").and_then(|v| v.as_str()).map(|s| s.to_string()),
skip_compile: args.get("skip_compile").and_then(|v| v.as_bool()).unwrap_or(false),
skip_clean: args.get("skip_clean").and_then(|v| v.as_bool()).unwrap_or(false),
skip_analyze: true, // Not needed for launch mode
deep_analyze: false,
shutdown: false, // Don't shutdown so we can poll
kill_existing: false,
timeout: args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(900),
gui: args.get("gui").and_then(|v| v.as_bool()).unwrap_or(false),
};
let pipeline = BacktestPipeline::new(config.clone());
let job = pipeline.launch_backtest(params).await?;
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"message": "Backtest launched successfully. Use get_backtest_status to poll for completion.",
"report_id": job.report_id,
"report_dir": job.report_dir,
"expert": job.expert,
"symbol": job.symbol,
"timeframe": job.timeframe,
"launched_at": job.launched_at,
"timeout_seconds": job.timeout_seconds,
"poll_hint": "Call get_backtest_status with report_dir to check progress"
}).to_string() }],
"isError": false
}))
}
pub async fn handle_get_backtest_status(_config: &Config, args: &Value) -> Result<Value> {
let report_dir = args.get("report_dir")
.and_then(|v| v.as_str())
.unwrap_or("latest");
let progress_file = Path::new(report_dir).join("progress.log");
let report_path = Path::new(report_dir);
let progress_file = report_path.join("progress.log");
let job_file = report_path.join("job.json");
let status = if progress_file.exists() {
// Load job info if available
let job: Option<BacktestJob> = if job_file.exists() {
fs::read_to_string(&job_file)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
} else {
None
};
// Check progress log for stage
let (stage, progress_lines) = if progress_file.exists() {
if let Ok(content) = fs::read_to_string(&progress_file) {
let last_line = content.lines().last().unwrap_or("");
if last_line.contains("DONE") {
"completed"
} else {
"running"
}
let lines: Vec<&str> = content.lines().collect();
let last_stage = lines.last()
.and_then(|l| l.split_whitespace().next())
.unwrap_or("UNKNOWN");
(last_stage.to_string(), lines.len())
} else {
"unknown"
("UNKNOWN".to_string(), 0)
}
} else {
("NOT_STARTED".to_string(), 0)
};
// Check if MT5 is running
let mt5_running = is_mt5_running();
// Check if report file exists
let report_found = job.as_ref()
.map(|j| Path::new(&j.expected_report_path).exists())
.unwrap_or(false);
// Check for completed artifacts
let metrics_exists = report_path.join("metrics.json").exists();
let deals_exists = report_path.join("deals.csv").exists();
let is_complete = stage == "DONE" || (report_found && metrics_exists);
// Calculate elapsed time if job exists
let elapsed_seconds = job.as_ref()
.and_then(|j| {
chrono::DateTime::parse_from_rfc3339(&j.launched_at)
.ok()
.map(|t| (chrono::Utc::now() - t.with_timezone(&chrono::Utc)).num_seconds())
})
.unwrap_or(0);
// Determine status message
let status_msg = if is_complete {
"completed"
} else if stage == "BACKTEST" && mt5_running {
"running"
} else if stage == "BACKTEST" && !mt5_running && !report_found {
"failed"
} else if progress_lines > 0 {
"in_progress"
} else {
"not_started"
};
let message = if is_complete {
"Backtest completed successfully"
} else if stage == "BACKTEST" && mt5_running {
"MT5 is running the backtest"
} else if stage == "BACKTEST" && !mt5_running {
"MT5 process exited but report not found - backtest may have failed"
} else {
&format!("Backtest is at stage: {}", stage)
};
Ok(json!({
"content": [{ "type": "text", "text": json!({
"success": true,
"report_dir": report_dir,
"status": status
"status": status_msg,
"stage": stage,
"is_complete": is_complete,
"mt5_running": mt5_running,
"report_found": report_found,
"metrics_extracted": metrics_exists,
"deals_extracted": deals_exists,
"elapsed_seconds": elapsed_seconds,
"message": message,
"job": job.map(|j| {
json!({
"report_id": j.report_id,
"expert": j.expert,
"symbol": j.symbol,
"timeframe": j.timeframe,
"launched_at": j.launched_at,
"timeout_seconds": j.timeout_seconds
})
})
}).to_string() }],
"isError": false
}))
}
/// Check if MT5 is currently running
fn is_mt5_running() -> bool {
let patterns = if cfg!(target_os = "macos") {
vec!["MetaTrader 5\\.app", "terminal64\\.exe"]
} else {
vec!["terminal64\\.exe", "metatrader"]
};
patterns.iter().any(|pat| {
Command::new("pgrep")
.args(["-f", pat])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
}
pub async fn handle_cache_status(config: &Config) -> Result<Value> {
let cache_dir = config.tester_cache_dir.as_ref()
.map(|s| Path::new(s))
+1 -1
View File
@@ -242,7 +242,7 @@ pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result<Value> {
let compiler = MqlCompiler::new(config.clone());
let expert_path = resolved_path.as_str();
match compiler.compile(&expert_path) {
match compiler.compile(&expert_path).await {
Ok(result) => {
Ok(json!({
"content": [{ "type": "text", "text": json!({
+5 -2
View File
@@ -42,8 +42,11 @@ impl ToolHandler {
"copy_indicator_to_project" => experts::handle_copy_indicator_to_project(&self.config, args).await,
"copy_script_to_project" => experts::handle_copy_script_to_project(&self.config, args).await,
// Backtest handlers
"run_backtest" => backtest::handle_run_backtest(&self.config, args).await,
// Backtest handlers - Granular pipeline options
"run_backtest" => backtest::handle_run_backtest(&self.config, args).await, // Full: compile + clean + backtest + extract + analyze
"run_backtest_quick" => backtest::handle_run_backtest_quick(&self.config, args).await, // Quick: skip compile, do backtest + extract + analyze
"run_backtest_only" => backtest::handle_run_backtest_only(&self.config, args).await, // Minimal: skip compile, do backtest + extract only
"launch_backtest" => backtest::handle_launch_backtest(&self.config, args).await, // Fire-and-forget mode
"get_backtest_status" => backtest::handle_get_backtest_status(&self.config, args).await,
"cache_status" => backtest::handle_cache_status(&self.config).await,
"clean_cache" => backtest::handle_clean_cache(&self.config, args).await,