diff --git a/src/models/config.rs b/src/models/config.rs index da629fb..27bf596 100644 --- a/src/models/config.rs +++ b/src/models/config.rs @@ -94,6 +94,7 @@ pub struct Config { pub backtest_timeout: Option, pub opt_log_dir: Option, pub opt_min_agents: Option, + pub opt_max_agents: Option, pub reports_dir: Option, pub backtest_login: Option, pub backtest_server: Option, @@ -121,6 +122,7 @@ impl Default for Config { backtest_timeout: None, opt_log_dir: None, opt_min_agents: None, + opt_max_agents: None, reports_dir: None, backtest_login: None, backtest_server: None, @@ -241,6 +243,7 @@ impl Config { cfg.backtest_timeout = Some(900); cfg.opt_log_dir = Some("/tmp".into()); cfg.opt_min_agents = Some(1); + cfg.opt_max_agents = Some(20); cfg } @@ -339,7 +342,8 @@ impl Config { backtest_timeout: {to}\n\ \n\ opt_log_dir: {opt_log}\n\ - opt_min_agents: {opt_agents}\n", + opt_min_agents: {opt_agents}\n\ + opt_max_agents: {max_agents}\n", wine = s(&self.wine_executable), term = s(&self.terminal_dir), exp = s(&self.experts_dir), @@ -357,6 +361,7 @@ impl Config { to = u(self.backtest_timeout), opt_log = s(&self.opt_log_dir), opt_agents = u(self.opt_min_agents), + max_agents = u(self.opt_max_agents), ); fs::write(&path, content)?; @@ -405,6 +410,7 @@ impl Config { backtest_timeout: map.get("backtest_timeout").and_then(|s| s.parse().ok()), opt_log_dir: map.get("opt_log_dir").cloned(), opt_min_agents: map.get("opt_min_agents").and_then(|s| s.parse().ok()), + opt_max_agents: map.get("opt_max_agents").and_then(|s| s.parse().ok()), reports_dir: map.get("reports_dir").cloned(), backtest_login: map.get("backtest_login").cloned(), backtest_server: map.get("backtest_server").cloned(), @@ -432,6 +438,7 @@ impl Config { "backtest_timeout" => self.backtest_timeout.unwrap_or(900).to_string(), "opt_log_dir" => self.opt_log_dir.clone().unwrap_or_else(|| "/tmp".to_string()), "opt_min_agents" => self.opt_min_agents.unwrap_or(1).to_string(), + "opt_max_agents" => self.opt_max_agents.unwrap_or(0).to_string(), "reports_dir" => self.reports_dir.clone().unwrap_or_else(|| "reports".to_string()), "backtest_login" => self.backtest_login.clone().unwrap_or_default(), "backtest_server" => self.backtest_server.clone().unwrap_or_default(), diff --git a/src/optimization/optimizer.rs b/src/optimization/optimizer.rs index 7bf0354..0492926 100644 --- a/src/optimization/optimizer.rs +++ b/src/optimization/optimizer.rs @@ -105,6 +105,11 @@ impl OptimizationRunner { let job_id = format!("opt_{}", timestamp); let log_file = PathBuf::from(format!("/tmp/mt5opt_{}.log", timestamp)); + // Calculate agent count: 75% of available CPUs, or configured value + let cpu_count = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1); + let default_agents = ((cpu_count as f64 * 0.75).ceil() as u32).max(1); + let max_agents = self.config.opt_max_agents.unwrap_or(default_agents).max(1); + // Count combinations let combinations = self.count_combinations(¶ms.set_file) .map_err(|e| anyhow!("count_combinations failed: {}", e))?; @@ -115,11 +120,22 @@ impl OptimizationRunner { let wine_exe = self.config.wine_executable.as_ref() .ok_or_else(|| anyhow!("wine_executable not configured"))?; + // Resolve the .set filename MT5 will actually load: basename of set_file, + // else "{expert}.set". Must match the name told to MT5 below (ExpertParameters). + let set_param = if !params.set_file.is_empty() && params.set_file != format!("{}.set", params.expert) { + std::path::Path::new(¶ms.set_file).file_name() + .and_then(|n| n.to_str()) + .unwrap_or(&format!("{}.set", params.expert)) + .to_string() + } else { + format!("{}.set", params.expert) + }; + // Write .set file as UTF-16LE with BOM directly to MT5 tester directory let wine_prefix_dir = self.get_wine_prefix_dir(mt5_dir)?; let tester_dir = wine_prefix_dir.join("drive_c/Program Files/MetaTrader 5/MQL5/Profiles/Tester"); fs::create_dir_all(&tester_dir).map_err(|e| anyhow!("create_dir_all({}) failed: {}", tester_dir.display(), e))?; - let dst_set_file = tester_dir.join(format!("{}.set", params.expert)); + let dst_set_file = tester_dir.join(&set_param); self.write_utf16le_set(¶ms.set_file, &dst_set_file) .map_err(|e| anyhow!("write_utf16le_set({}) failed: {}", dst_set_file.display(), e))?; @@ -145,24 +161,13 @@ impl OptimizationRunner { } else { format!("Experts\\{}.ex5", params.expert) }; - let set_param = { - let base = if !params.set_file.is_empty() && params.set_file != format!("{}.set", params.expert) { - // Extract just the filename from the set file path - std::path::Path::new(¶ms.set_file).file_name() - .and_then(|n| n.to_str()) - .unwrap_or(&format!("{}.set", params.expert)) - .to_string() - } else { - format!("{}.set", params.expert) - }; - base - }; let mut tester_section = format!( "[Tester]\n\ Expert={}\n\ ExpertParameters={}\n\ Symbol={}\n\ Period=M1\n\ + LocalAgents={}\n\ Model=1\n\ FromDate={}\n\ ToDate={}\n\ @@ -177,7 +182,7 @@ impl OptimizationRunner { Report=..\\..\\mt5mcp_opt_report.htm\n\ ReplaceReport=1\n\ ShutdownTerminal=1", - expert_path, set_param, params.symbol, + expert_path, set_param, params.symbol, max_agents, params.from_date, params.to_date, params.deposit, params.currency, params.leverage, ); if let Some(mp) = params.max_passes { @@ -211,8 +216,9 @@ impl OptimizationRunner { opt_ini.push_str(&format!("Expert={}.ex5\n", params.expert)); opt_ini.push_str(&format!("ExpertParameters={}\n", set_param)); opt_ini.push_str(&format!("Symbol={}\n", params.symbol)); - opt_ini.push_str("Period=M1\n"); - opt_ini.push_str("Model=1\n"); + opt_ini.push_str(&format!("Period={}\n", "M1")); + opt_ini.push_str(&format!("LocalAgents={}\n", max_agents)); + opt_ini.push_str(&format!("Model={}\n", "0")); opt_ini.push_str("Optimization=2\n"); opt_ini.push_str(&format!("FromDate={}\n", params.from_date)); opt_ini.push_str(&format!("ToDate={}\n", params.to_date)); @@ -249,12 +255,13 @@ impl OptimizationRunner { export DYLD_FALLBACK_LIBRARY_PATH='{dyld}'\n\ export WINEPREFIX='{prefix}'\n\ export WINEDEBUG='-all'\n\ - nohup '{wine}' '{terminal}' '/config:{config}' >/dev/null 2>&1 &\n", + nohup taskset -c 0-$(( {max_agents} - 1 )) '{wine}' '{terminal}' '/config:{config}' >/dev/null 2>&1 &\n", dyld = dyld, prefix = wine_prefix_dir.display(), wine = wine_exe, terminal = terminal_host.display(), config = opt_config_win, + max_agents = max_agents, ); let script_path = std::env::temp_dir().join("mt5opt_launch.sh"); @@ -368,8 +375,8 @@ impl OptimizationRunner { combinations: u64, wine_prefix: &Path, ) -> Result<()> { - let jobs_dir = Path::new(".mt5mcp_jobs"); - fs::create_dir_all(jobs_dir)?; + let jobs_dir = std::env::temp_dir().join(".mt5mcp_jobs"); + fs::create_dir_all(&jobs_dir)?; let meta_path = jobs_dir.join(format!("{}.json", job_id)); let started_at = Utc::now().to_rfc3339(); @@ -462,7 +469,7 @@ impl OptimizationRunner { } pub fn get_job_status(&self, job_id: &str) -> Result { - let jobs_dir = Path::new(".mt5mcp_jobs"); + let jobs_dir = std::env::temp_dir().join(".mt5mcp_jobs"); let meta_path = jobs_dir.join(format!("{}.json", job_id)); if !meta_path.exists() { @@ -538,7 +545,7 @@ impl OptimizationRunner { } pub fn list_jobs(&self) -> Result> { - let jobs_dir = Path::new(".mt5mcp_jobs"); + let jobs_dir = std::env::temp_dir().join(".mt5mcp_jobs"); let mut jobs = Vec::new(); if jobs_dir.exists() { diff --git a/src/optimization/parser.rs b/src/optimization/parser.rs index ab4943d..0c0cbbb 100644 --- a/src/optimization/parser.rs +++ b/src/optimization/parser.rs @@ -23,7 +23,7 @@ impl OptimizationParser { } pub fn parse_job(&self, job_id: &str) -> Result> { - let jobs_dir = Path::new(".mt5mcp_jobs"); + let jobs_dir = std::env::temp_dir().join(".mt5mcp_jobs"); let meta_path = jobs_dir.join(format!("{}.json", job_id)); if !meta_path.exists() { @@ -188,14 +188,13 @@ impl OptimizationParser { for node in doc.descendants() { if node.has_tag_name(("urn:schemas-microsoft-com:office:spreadsheet", "Row")) || node.has_tag_name("Row") { - let cells: Vec = node.children() + let cells: Vec = node.descendants() .filter(|n: &roxmltree::Node<'_, '_>| { - n.has_tag_name(("urn:schemas-microsoft-com:office:spreadsheet", "Cell")) || - n.has_tag_name("Cell") || n.has_tag_name(("urn:schemas-microsoft-com:office:spreadsheet", "Data")) || n.has_tag_name("Data") }) - .map(|n| n.text().unwrap_or("").trim().to_string().replace(',', "")) + .filter_map(|n| n.text()) + .map(|t| t.trim().to_string().replace(',', "")) .collect(); if cells.is_empty() { diff --git a/src/tools/handlers/optimization.rs b/src/tools/handlers/optimization.rs index 21b9933..dced176 100644 --- a/src/tools/handlers/optimization.rs +++ b/src/tools/handlers/optimization.rs @@ -60,7 +60,7 @@ pub async fn handle_get_optimization_status(config: &Config, args: &Value) -> Re // Store completion results back to metadata for persistence if status.get("status").and_then(|v| v.as_str()) == Some("completed") { - let jobs_dir = Path::new(".mt5mcp_jobs"); + let jobs_dir = std::env::temp_dir().join(".mt5mcp_jobs"); let meta_path = jobs_dir.join(format!("{}.json", job_id)); if let Ok(meta_str) = fs::read_to_string(&meta_path) { if let Ok(mut meta) = serde_json::from_str::(&meta_str) { diff --git a/src/tools/handlers/reports.rs b/src/tools/handlers/reports.rs index 7519862..ed479d1 100644 --- a/src/tools/handlers/reports.rs +++ b/src/tools/handlers/reports.rs @@ -237,7 +237,7 @@ pub async fn handle_tail_log(_config: &Config, args: &Value) -> Result { let lines = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(50) as usize; let log_path = if let Some(jid) = job_id { - let jobs_dir = Path::new(".mt5mcp_jobs"); + let jobs_dir = std::env::temp_dir().join(".mt5mcp_jobs"); let meta_path = jobs_dir.join(format!("{}.json", jid)); let meta: Value = serde_json::from_str(&fs::read_to_string(meta_path)?)?; meta.get("log_file").and_then(|v| v.as_str()).map(|s| s.to_string())