feat: configurable local agents + CPU pinning for optimization
- Add opt_max_agents config (default 75% of CPUs, capped at 20) - Set LocalAgents in tester .ini and pin MT5 to CPUs 0..max_agents via taskset - Resolve .set filename consistently between tester + ini config - Move .mt5mcp_jobs to temp dir for persistence across CWD changes - Fix optimization XML row parsing to descend into Cell/Data nodes
This commit is contained in:
@@ -94,6 +94,7 @@ pub struct Config {
|
||||
pub backtest_timeout: Option<u32>,
|
||||
pub opt_log_dir: Option<String>,
|
||||
pub opt_min_agents: Option<u32>,
|
||||
pub opt_max_agents: Option<u32>,
|
||||
pub reports_dir: Option<String>,
|
||||
pub backtest_login: Option<String>,
|
||||
pub backtest_server: Option<String>,
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<serde_json::Value> {
|
||||
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<Vec<serde_json::Value>> {
|
||||
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() {
|
||||
|
||||
@@ -23,7 +23,7 @@ impl OptimizationParser {
|
||||
}
|
||||
|
||||
pub fn parse_job(&self, job_id: &str) -> Result<Vec<OptimizationPass>> {
|
||||
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<String> = node.children()
|
||||
let cells: Vec<String> = 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() {
|
||||
|
||||
@@ -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::<serde_json::Value>(&meta_str) {
|
||||
|
||||
@@ -237,7 +237,7 @@ pub async fn handle_tail_log(_config: &Config, args: &Value) -> Result<Value> {
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user