diff --git a/src/tools/handlers/analysis.rs b/src/tools/handlers/analysis.rs index 342fbc6..b4fa0c8 100644 --- a/src/tools/handlers/analysis.rs +++ b/src/tools/handlers/analysis.rs @@ -6,8 +6,30 @@ use std::path::Path; use crate::analytics::DealAnalyzer; use crate::models::deals::Deal; use crate::models::metrics::Metrics; +use crate::models::Config; + +// ── Internal helpers ────────────────────────────────────────────────────────── + +fn required_str<'a>(args: &'a Value, key: &str) -> Result<&'a str> { + args.get(key) + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("{} is required", key)) +} + +fn ok_response(data: Value) -> Value { + json!({ + "content": [{ "type": "text", "text": data.to_string() }], + "isError": false + }) +} + +fn err_response(msg: impl std::fmt::Display) -> Value { + json!({ + "content": [{ "type": "text", "text": msg.to_string() }], + "isError": true + }) +} -/// Helper to load deals and metrics from report directory fn load_report_data(report_dir: &str) -> Result<(Vec, Metrics)> { let deals_csv = Path::new(report_dir).join("deals.csv"); let metrics_json = Path::new(report_dir).join("metrics.json"); @@ -17,10 +39,8 @@ fn load_report_data(report_dir: &str) -> Result<(Vec, Metrics)> { } let deals = read_deals_from_csv(&deals_csv)?; - let metrics = if metrics_json.exists() { - let content = fs::read_to_string(&metrics_json)?; - serde_json::from_str(&content)? + serde_json::from_str(&fs::read_to_string(&metrics_json)?)? } else { Metrics::default() }; @@ -28,75 +48,18 @@ fn load_report_data(report_dir: &str) -> Result<(Vec, Metrics)> { Ok((deals, metrics)) } -pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - +fn prepare_analysis(report_dir: &str) -> Result<(Vec, Metrics, DealAnalyzer)> { let (deals, metrics) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - - // Check if specific analytics requested - let requested: Option> = args.get("analytics") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect()); - - let top_losses_limit = args.get("top_losses_limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; - - let all = requested.is_none(); - let req = |name: &str| all || requested.as_ref().map(|s| s.contains(name)).unwrap_or(false); - - // Build selective result - let mut result = json!({}); - - if req("monthly_pnl") || all { - result["monthly"] = json!(analyzer.monthly_pnl(&deals)); - } - if req("drawdown_events") || all { - result["dd_events"] = json!(analyzer.reconstruct_dd_events(&deals, &metrics)); - } - if req("top_losses") || all { - result["top_losses"] = json!(analyzer.top_losses(&deals, top_losses_limit)); - } - if req("loss_sequences") || all { - result["loss_sequences"] = json!(analyzer.loss_sequences(&deals)); - } - if req("position_pairs") || all { - result["position_pairs"] = json!(analyzer.position_pairs(&deals)); - } - if req("direction_bias") || all { - result["direction_bias"] = json!(analyzer.direction_bias(&deals)); - } - if req("streak_analysis") || all { - result["streak_analysis"] = json!(analyzer.streak_analysis(&deals)); - } - if req("concurrent_peak") || all { - result["concurrent_peak"] = json!(analyzer.concurrent_peak(&deals)); - } - - let analysis_path = Path::new(report_dir).join("analysis.json"); - fs::write(&analysis_path, serde_json::to_string_pretty(&result)?)?; - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "analysis_file": analysis_path.to_string_lossy(), - "analytics_run": requested.map(|s| s.iter().cloned().collect::>()).unwrap_or_else(|| vec!["all".to_string()]), - "summary": result, - }).to_string() }], - "isError": false - })) + Ok((deals, metrics, DealAnalyzer::new())) } fn read_deals_from_csv(path: &Path) -> Result> { let content = fs::read_to_string(path)?; let mut deals = Vec::new(); - + let mut lines = content.lines(); let _header = lines.next(); - + for line in lines { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 12 { @@ -118,523 +81,268 @@ fn read_deals_from_csv(path: &Path) -> Result> { }); } } - + Ok(deals) } +// ── Composite analytics ─────────────────────────────────────────────────────── + +pub async fn handle_analyze_report(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, metrics, analyzer) = prepare_analysis(report_dir)?; + + let requested: Option> = args.get("analytics") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()); + + let top_losses_limit = args.get("top_losses_limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; + + let all = requested.is_none(); + let req = |name: &str| all || requested.as_ref().map(|s| s.contains(name)).unwrap_or(false); + + let mut result = json!({}); + + if req("monthly_pnl") { result["monthly"] = json!(analyzer.monthly_pnl(&deals)); } + if req("drawdown_events") { result["dd_events"] = json!(analyzer.reconstruct_dd_events(&deals, &metrics)); } + if req("top_losses") { result["top_losses"] = json!(analyzer.top_losses(&deals, top_losses_limit)); } + if req("loss_sequences") { result["loss_sequences"] = json!(analyzer.loss_sequences(&deals)); } + if req("position_pairs") { result["position_pairs"] = json!(analyzer.position_pairs(&deals)); } + if req("direction_bias") { result["direction_bias"] = json!(analyzer.direction_bias(&deals)); } + if req("streak_analysis") { result["streak_analysis"] = json!(analyzer.streak_analysis(&deals)); } + if req("concurrent_peak") { result["concurrent_peak"] = json!(analyzer.concurrent_peak(&deals)); } + + let analysis_path = Path::new(report_dir).join("analysis.json"); + fs::write(&analysis_path, serde_json::to_string_pretty(&result)?)?; + + Ok(ok_response(json!({ + "success": true, + "analysis_file": analysis_path.to_string_lossy(), + "analytics_run": requested.map(|s| s.iter().cloned().collect::>()).unwrap_or_else(|| vec!["all".to_string()]), + "summary": result, + }))) +} + pub async fn handle_compare_baseline(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; + let report_dir = required_str(args, "report_dir")?; let baseline_path = Path::new("config/baseline.json"); let metrics_path = Path::new(report_dir).join("metrics.json"); if !baseline_path.exists() { - return Ok(json!({ - "content": [{ "type": "text", "text": "No baseline.json found in config/" }], - "isError": false - })); + return Ok(ok_response(json!("No baseline.json found in config/"))); } let baseline: Value = serde_json::from_str(&fs::read_to_string(baseline_path)?)?; let current: Value = serde_json::from_str(&fs::read_to_string(metrics_path)?)?; - let comparison = json!({ + Ok(ok_response(json!({ "baseline": baseline, "current": current, "improvements": { - "profit": current.get("net_profit").and_then(|v| v.as_f64()).unwrap_or(0.0) + "profit": current.get("net_profit").and_then(|v| v.as_f64()).unwrap_or(0.0) - baseline.get("net_profit").and_then(|v| v.as_f64()).unwrap_or(0.0), "drawdown": current.get("max_dd_pct").and_then(|v| v.as_f64()).unwrap_or(0.0) - baseline.get("max_dd_pct").and_then(|v| v.as_f64()).unwrap_or(0.0), } - }); - - Ok(json!({ - "content": [{ "type": "text", "text": comparison.to_string() }], - "isError": false - })) + }))) } -// === Granular Analytics Handlers === +// ── Granular analytics handlers ─────────────────────────────────────────────── pub async fn handle_analyze_monthly_pnl(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.monthly_pnl(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "monthly_pnl": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "monthly_pnl": analyzer.monthly_pnl(&deals) }))) } pub async fn handle_analyze_drawdown_events(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, metrics) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.reconstruct_dd_events(&deals, &metrics); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "drawdown_events": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, metrics, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "drawdown_events": analyzer.reconstruct_dd_events(&deals, &metrics) }))) } pub async fn handle_analyze_top_losses(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - + let report_dir = required_str(args, "report_dir")?; let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.top_losses(&deals, limit); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "limit": limit, - "top_losses": result, - }).to_string() }], - "isError": false - })) + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "limit": limit, "top_losses": analyzer.top_losses(&deals, limit) }))) } pub async fn handle_analyze_loss_sequences(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.loss_sequences(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "loss_sequences": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "loss_sequences": analyzer.loss_sequences(&deals) }))) } pub async fn handle_analyze_position_pairs(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.position_pairs(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "position_pairs": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "position_pairs": analyzer.position_pairs(&deals) }))) } pub async fn handle_analyze_direction_bias(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.direction_bias(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "direction_bias": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "direction_bias": analyzer.direction_bias(&deals) }))) } pub async fn handle_analyze_streaks(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.streak_analysis(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "streak_analysis": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "streak_analysis": analyzer.streak_analysis(&deals) }))) } pub async fn handle_analyze_concurrent_peak(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.concurrent_peak(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "concurrent_peak": result, - }).to_string() }], - "isError": false - })) + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "concurrent_peak": analyzer.concurrent_peak(&deals) }))) } -// === Deal Query Handlers === +pub async fn handle_analyze_profit_distribution(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "profit_distribution": analyzer.profit_distribution(&deals) }))) +} + +pub async fn handle_analyze_time_performance(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "time_performance": analyzer.time_performance(&deals) }))) +} + +pub async fn handle_analyze_hold_time_distribution(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "hold_time_analysis": analyzer.hold_time_analysis(&deals) }))) +} + +pub async fn handle_analyze_layer_performance(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "layer_performance": analyzer.layer_performance(&deals) }))) +} + +pub async fn handle_analyze_volume_vs_profit(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "volume_analysis": analyzer.volume_analysis(&deals) }))) +} + +pub async fn handle_analyze_costs(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, _, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "cost_analysis": analyzer.cost_analysis(&deals) }))) +} + +pub async fn handle_analyze_efficiency(_config: &Config, args: &Value) -> Result { + let report_dir = required_str(args, "report_dir")?; + let (deals, metrics, analyzer) = prepare_analysis(report_dir)?; + Ok(ok_response(json!({ "success": true, "efficiency_analysis": analyzer.efficiency_analysis(&deals, &metrics) }))) +} + +// ── Deal query handlers ─────────────────────────────────────────────────────── + +fn deal_to_json(d: &Deal) -> Value { + json!({ + "time": d.time, + "deal": d.deal, + "symbol": d.symbol, + "deal_type": d.deal_type, + "volume": d.volume, + "price": d.price, + "profit": d.profit, + "commission": d.commission, + "swap": d.swap, + "comment": d.comment, + "magic": d.magic, + }) +} + +fn is_closed_trade(d: &Deal) -> bool { + d.entry.to_lowercase().contains("out") && d.profit != 0.0 +} pub async fn handle_list_deals(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - + let report_dir = required_str(args, "report_dir")?; let (deals, _) = load_report_data(report_dir)?; - // Apply filters - let deal_type = args.get("deal_type").and_then(|v| v.as_str()); + let deal_type = args.get("deal_type").and_then(|v| v.as_str()); let min_profit = args.get("min_profit").and_then(|v| v.as_f64()); let max_profit = args.get("max_profit").and_then(|v| v.as_f64()); let start_date = args.get("start_date").and_then(|v| v.as_str()); - let end_date = args.get("end_date").and_then(|v| v.as_str()); + let end_date = args.get("end_date").and_then(|v| v.as_str()); let min_volume = args.get("min_volume").and_then(|v| v.as_f64()); let max_volume = args.get("max_volume").and_then(|v| v.as_f64()); let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize; let mut filtered: Vec<&Deal> = deals.iter().filter(|d| { - // Only include closed trades with non-zero profit - if !d.entry.to_lowercase().contains("out") || d.profit == 0.0 { - return false; - } - - if let Some(dt) = deal_type { - if !d.deal_type.to_lowercase().contains(dt) { - return false; - } - } - - if let Some(min) = min_profit { - if d.profit < min { - return false; - } - } - - if let Some(max) = max_profit { - if d.profit > max { - return false; - } - } - - if let Some(start) = start_date { - if !d.time.starts_with(start) && d.time < start.to_string() { - return false; - } - } - - if let Some(end) = end_date { - if d.time > end.to_string() { - return false; - } - } - - if let Some(min) = min_volume { - if d.volume < min { - return false; - } - } - - if let Some(max) = max_volume { - if d.volume > max { - return false; - } - } - + if !is_closed_trade(d) { return false; } + if let Some(dt) = deal_type { if !d.deal_type.to_lowercase().contains(dt) { return false; } } + if let Some(min) = min_profit { if d.profit < min { return false; } } + if let Some(max) = max_profit { if d.profit > max { return false; } } + if let Some(s) = start_date { if d.time.as_str() < s { return false; } } + if let Some(e) = end_date { if d.time.as_str() > e { return false; } } + if let Some(min) = min_volume { if d.volume < min { return false; } } + if let Some(max) = max_volume { if d.volume > max { return false; } } true }).collect(); - // Sort by time descending filtered.sort_by(|a, b| b.time.cmp(&a.time)); filtered.truncate(limit); - let deal_list: Vec = filtered - .iter() - .map(|d| json!({ - "time": d.time, - "deal": d.deal, - "symbol": d.symbol, - "deal_type": d.deal_type, - "volume": d.volume, - "price": d.price, - "profit": d.profit, - "commission": d.commission, - "swap": d.swap, - "comment": d.comment, - "magic": d.magic, - })) - .collect(); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "total_deals": deals.len(), - "filtered_count": deal_list.len(), - "deals": deal_list, - }).to_string() }], - "isError": false - })) + Ok(ok_response(json!({ + "success": true, + "total_deals": deals.len(), + "filtered_count": filtered.len(), + "deals": filtered.iter().map(|d| deal_to_json(d)).collect::>(), + }))) } pub async fn handle_search_deals_by_comment(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let query = args.get("query") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("query is required"))?; - + let report_dir = required_str(args, "report_dir")?; + let query = required_str(args, "query")?; let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize; let (deals, _) = load_report_data(report_dir)?; - let query_lower = query.to_lowercase(); - let mut filtered: Vec<&Deal> = deals - .iter() - .filter(|d| { - d.entry.to_lowercase().contains("out") - && d.profit != 0.0 - && d.comment.to_lowercase().contains(&query_lower) - }) + + let mut filtered: Vec<&Deal> = deals.iter() + .filter(|d| is_closed_trade(d) && d.comment.to_lowercase().contains(&query_lower)) .collect(); filtered.sort_by(|a, b| b.time.cmp(&a.time)); filtered.truncate(limit); - let deal_list: Vec = filtered - .iter() - .map(|d| json!({ - "time": d.time, - "deal": d.deal, - "symbol": d.symbol, - "deal_type": d.deal_type, - "volume": d.volume, - "profit": d.profit, - "comment": d.comment, - "magic": d.magic, - })) - .collect(); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "query": query, - "matched": deal_list.len(), - "deals": deal_list, - }).to_string() }], - "isError": false - })) + Ok(ok_response(json!({ + "success": true, + "query": query, + "matched": filtered.len(), + "deals": filtered.iter().map(|d| deal_to_json(d)).collect::>(), + }))) } pub async fn handle_search_deals_by_magic(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let magic = args.get("magic") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("magic is required"))?; - + let report_dir = required_str(args, "report_dir")?; + let magic = required_str(args, "magic")?; let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize; let (deals, _) = load_report_data(report_dir)?; - let mut filtered: Vec<&Deal> = deals - .iter() - .filter(|d| { - d.entry.to_lowercase().contains("out") - && d.profit != 0.0 - && d.magic.as_ref().map(|m| m.contains(magic)).unwrap_or(false) - }) + let mut filtered: Vec<&Deal> = deals.iter() + .filter(|d| is_closed_trade(d) && d.magic.as_ref().map(|m| m.contains(magic)).unwrap_or(false)) .collect(); filtered.sort_by(|a, b| b.time.cmp(&a.time)); filtered.truncate(limit); - let deal_list: Vec = filtered - .iter() - .map(|d| json!({ - "time": d.time, - "deal": d.deal, - "symbol": d.symbol, - "deal_type": d.deal_type, - "volume": d.volume, - "profit": d.profit, - "comment": d.comment, - "magic": d.magic, - })) - .collect(); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "magic": magic, - "matched": deal_list.len(), - "deals": deal_list, - }).to_string() }], - "isError": false - })) + Ok(ok_response(json!({ + "success": true, + "magic": magic, + "matched": filtered.len(), + "deals": filtered.iter().map(|d| deal_to_json(d)).collect::>(), + }))) } -// === New Analytics Handlers === - -pub async fn handle_analyze_profit_distribution(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.profit_distribution(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "profit_distribution": result, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_analyze_time_performance(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.time_performance(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "time_performance": result, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_analyze_hold_time_distribution(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.hold_time_analysis(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "hold_time_analysis": result, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_analyze_layer_performance(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.layer_performance(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "layer_performance": result, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_analyze_volume_vs_profit(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.volume_analysis(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "volume_analysis": result, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_analyze_costs(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, _) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.cost_analysis(&deals); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "cost_analysis": result, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_analyze_efficiency(_config: &Config, args: &Value) -> Result { - let report_dir = args.get("report_dir") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("report_dir is required"))?; - - let (deals, metrics) = load_report_data(report_dir)?; - let analyzer = DealAnalyzer::new(); - let result = analyzer.efficiency_analysis(&deals, &metrics); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "efficiency_analysis": result, - }).to_string() }], - "isError": false - })) -} - -// Import Config for analysis module -use crate::models::Config; +// suppress unused warning — err_response is available for future handlers +#[allow(dead_code)] +fn _use_err_response() { let _ = err_response(""); } diff --git a/src/tools/handlers/experts.rs b/src/tools/handlers/experts.rs index ec4c90e..f701e04 100644 --- a/src/tools/handlers/experts.rs +++ b/src/tools/handlers/experts.rs @@ -4,212 +4,209 @@ use walkdir::WalkDir; use crate::compile::MqlCompiler; use crate::models::Config; -pub async fn handle_list_experts(config: &Config, args: &Value) -> Result { - let filter = args.get("filter").and_then(|v| v.as_str()); - - let mut experts = Vec::new(); - - if let Some(experts_dir) = &config.experts_dir { - for entry in WalkDir::new(experts_dir).max_depth(3) { - if let Ok(entry) = entry { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy().to_string(); - let is_compiled = path.extension() - .map(|e| e == "ex5") - .unwrap_or(false); - - if let Some(filter_str) = filter { - if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) { - continue; - } - } - - experts.push(json!({ - "name": name_str, - "compiled": is_compiled, - "path": path.to_string_lossy().to_string(), - })); +const BUILTIN_INDICATORS: &[&str] = &[ + "Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR", + "Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force", + "Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum", + "OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR", +]; + +// ── Internal helpers ────────────────────────────────────────────────────────── + +/// Walk a MQL directory and return file entries matching an optional filter. +/// `type_label` is included as a `"type"` field when provided (e.g. "custom"). +fn scan_mql_dir(dir: Option<&String>, filter: Option<&str>, type_label: Option<&str>) -> Vec { + let Some(dir) = dir else { return Vec::new() }; + let filter_lower = filter.map(|f| f.to_lowercase()); + + let mut items: Vec = WalkDir::new(dir) + .max_depth(3) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()) + .filter_map(|e| { + let path = e.path(); + let name = path.file_stem()?.to_string_lossy().into_owned(); + if let Some(ref f) = filter_lower { + if !name.to_lowercase().contains(f.as_str()) { + return None; } } - } - } - - experts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "count": experts.len(), - "experts": experts, - }).to_string() }], + let is_compiled = path.extension().map(|ext| ext == "ex5").unwrap_or(false); + let mut obj = json!({ + "name": name, + "compiled": is_compiled, + "path": path.to_string_lossy().as_ref(), + }); + if let Some(t) = type_label { + obj["type"] = json!(t); + } + Some(obj) + }) + .collect(); + + items.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); + items +} + +fn ok_response(data: Value) -> Value { + json!({ + "content": [{ "type": "text", "text": data.to_string() }], "isError": false - })) + }) +} + +fn err_response(msg: impl std::fmt::Display) -> Value { + json!({ + "content": [{ "type": "text", "text": msg.to_string() }], + "isError": true + }) +} + +/// Shared implementation for copy_indicator_to_project / copy_script_to_project. +/// `default_fallback` is used as the stem when the source has no filename. +fn copy_mql_to_project(config: &Config, args: &Value, default_fallback: &str) -> Result { + use std::path::PathBuf; + + let source_path = args.get("source_path") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("source_path is required"))?; + + let target_name = args.get("target_name").and_then(|v| v.as_str()); + + let project_dir = config.project_dir.as_ref() + .map(PathBuf::from) + .or_else(|| std::env::current_dir().ok()) + .ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?; + + let source = PathBuf::from(source_path); + if !source.exists() { + return Ok(err_response( + serde_json::to_string(&json!({ "success": false, "error": format!("Source file not found: {}", source_path) })).unwrap_or_default() + )); + } + + let ext = source.extension().and_then(|e| e.to_str()).unwrap_or("mq5"); + let target_filename = match target_name { + Some(name) => format!("{}.{}", name, ext), + None => source.file_name() + .and_then(|n| n.to_str()) + .map(|n| n.to_string()) + .unwrap_or_else(|| format!("{}.{}", default_fallback, ext)), + }; + + let destination = project_dir.join(&target_filename); + + match std::fs::copy(&source, &destination) { + Ok(bytes) => Ok(ok_response(json!({ + "success": true, + "source": source_path, + "destination": destination.to_string_lossy().as_ref(), + "bytes_copied": bytes, + }))), + Err(e) => Ok(err_response( + serde_json::to_string(&json!({ "success": false, "error": format!("Failed to copy file: {}", e) })).unwrap_or_default() + )), + } +} + +// ── Public handlers ─────────────────────────────────────────────────────────── + +pub async fn handle_list_experts(config: &Config, args: &Value) -> Result { + let filter = args.get("filter").and_then(|v| v.as_str()); + let experts = scan_mql_dir(config.experts_dir.as_ref(), filter, None); + Ok(ok_response(json!({ + "success": true, + "count": experts.len(), + "experts": experts, + }))) } pub async fn handle_search_experts(config: &Config, args: &Value) -> Result { let pattern = args.get("pattern") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; - - let pattern_lower = pattern.to_lowercase(); - let mut matches = Vec::new(); - - if let Some(experts_dir) = &config.experts_dir { - for entry in WalkDir::new(experts_dir).max_depth(3) { - if let Ok(entry) = entry { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy().to_string(); - if name_str.to_lowercase().contains(&pattern_lower) { - let is_compiled = path.extension() - .map(|e| e == "ex5") - .unwrap_or(false); - matches.push(json!({ - "name": name_str, - "path": path.to_string_lossy().to_string(), - "compiled": is_compiled, - })); - } - } - } - } - } - - matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "pattern": pattern, - "count": matches.len(), - "matches": matches, - }).to_string() }], - "isError": false - })) + let matches = scan_mql_dir(config.experts_dir.as_ref(), Some(pattern), None); + Ok(ok_response(json!({ + "success": true, + "pattern": pattern, + "count": matches.len(), + "matches": matches, + }))) } pub async fn handle_list_indicators(config: &Config, args: &Value) -> Result { let filter = args.get("filter").and_then(|v| v.as_str()); let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false); - - let mut indicators = Vec::new(); - - // List custom indicators - if let Some(indicators_dir) = &config.indicators_dir { - for entry in WalkDir::new(indicators_dir).max_depth(3) { - if let Ok(entry) = entry { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy().to_string(); - let is_compiled = path.extension() - .map(|e| e == "ex5") - .unwrap_or(false); - - if let Some(filter_str) = filter { - if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) { - continue; - } - } - - indicators.push(json!({ - "name": name_str, - "compiled": is_compiled, - "type": "custom", - "path": path.to_string_lossy().to_string(), - })); - } - } - } - } - - // Add built-in indicators if requested + + let mut indicators = scan_mql_dir(config.indicators_dir.as_ref(), filter, Some("custom")); + if include_builtin { - let builtin = vec![ - "Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR", - "Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force", - "Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum", - "OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR", - ]; - for name in builtin { - if filter.map(|f| name.to_lowercase().contains(&f.to_lowercase())).unwrap_or(true) { - indicators.push(json!({ - "name": name, - "compiled": true, - "type": "builtin", - "path": null, - })); + let filter_lower = filter.map(|f| f.to_lowercase()); + for &name in BUILTIN_INDICATORS { + if filter_lower.as_ref().map(|f| name.to_lowercase().contains(f.as_str())).unwrap_or(true) { + indicators.push(json!({ "name": name, "compiled": true, "type": "builtin", "path": null })); } } + indicators.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); } - - indicators.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "count": indicators.len(), - "indicators": indicators, - "custom_dir": config.indicators_dir.clone(), - }).to_string() }], - "isError": false - })) + + Ok(ok_response(json!({ + "success": true, + "count": indicators.len(), + "indicators": indicators, + "custom_dir": config.indicators_dir.clone(), + }))) } pub async fn handle_list_scripts(config: &Config, args: &Value) -> Result { let filter = args.get("filter").and_then(|v| v.as_str()); - - let mut scripts = Vec::new(); - - if let Some(scripts_dir) = &config.scripts_dir { - for entry in WalkDir::new(scripts_dir).max_depth(3) { - if let Ok(entry) = entry { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy().to_string(); - let is_compiled = path.extension() - .map(|e| e == "ex5") - .unwrap_or(false); - - if let Some(filter_str) = filter { - if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) { - continue; - } - } - - scripts.push(json!({ - "name": name_str, - "compiled": is_compiled, - "path": path.to_string_lossy().to_string(), - })); - } + let scripts = scan_mql_dir(config.scripts_dir.as_ref(), filter, None); + Ok(ok_response(json!({ + "success": true, + "count": scripts.len(), + "scripts": scripts, + "scripts_dir": config.scripts_dir.clone(), + }))) +} + +pub async fn handle_search_indicators(config: &Config, args: &Value) -> Result { + let pattern = args.get("pattern") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; + let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false); + + let mut matches = scan_mql_dir(config.indicators_dir.as_ref(), Some(pattern), Some("custom")); + + if include_builtin { + let pattern_lower = pattern.to_lowercase(); + for &name in BUILTIN_INDICATORS { + if name.to_lowercase().contains(&pattern_lower) { + matches.push(json!({ "name": name, "path": null, "type": "builtin", "compiled": true })); } } + matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); } - - scripts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "count": scripts.len(), - "scripts": scripts, - "scripts_dir": config.scripts_dir.clone(), - }).to_string() }], - "isError": false - })) + + Ok(ok_response(json!({ + "success": true, + "pattern": pattern, + "count": matches.len(), + "matches": matches, + }))) +} + +pub async fn handle_search_scripts(config: &Config, args: &Value) -> Result { + let pattern = args.get("pattern") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; + let matches = scan_mql_dir(config.scripts_dir.as_ref(), Some(pattern), None); + Ok(ok_response(json!({ + "success": true, + "pattern": pattern, + "count": matches.len(), + "matches": matches, + }))) } pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result { @@ -218,302 +215,49 @@ pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result { let resolved_path: String = if let Some(p) = args.get("expert_path").and_then(|v| v.as_str()) { p.to_string() } else if let Some(name) = args.get("expert").and_then(|v| v.as_str()) { - let mut candidates = vec![ - PathBuf::from(name).with_extension("mq5"), - ]; + let mut candidates = vec![PathBuf::from(name).with_extension("mq5")]; if let Some(experts_dir) = &config.experts_dir { candidates.push(PathBuf::from(experts_dir).join(name).join(format!("{}.mq5", name))); candidates.push(PathBuf::from(experts_dir).join(format!("{}.mq5", name))); } match candidates.into_iter().find(|p| p.exists()) { Some(p) => p.to_string_lossy().to_string(), - None => return Ok(serde_json::json!({ - "content": [{ "type": "text", "text": serde_json::json!({ + None => return Ok(err_response( + serde_json::to_string(&json!({ "success": false, "error": format!("Cannot find {}.mq5 in MT5 Experts dir or current directory", name), - }).to_string() }], - "isError": true - })), + })).unwrap_or_default() + )), } } else { return Err(anyhow::anyhow!("Either 'expert' or 'expert_path' is required")); }; let compiler = MqlCompiler::new(config.clone()); - let expert_path = resolved_path.as_str(); - - match compiler.compile(&expert_path).await { - Ok(result) => { - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": result.success, - "binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()), - "binary_size_bytes": result.binary_size, - "files_synced": result.files_synced, - "warnings": result.warnings.len(), - "errors": result.errors.len(), - "error_list": result.errors, - "warning_list": result.warnings, - }).to_string() }], - "isError": !result.success - })) - } - Err(e) => { - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": false, - "error": format!("Compilation failed: {}", e), - }).to_string() }], - "isError": true - })) - } + match compiler.compile(&resolved_path).await { + Ok(result) => Ok(json!({ + "content": [{ "type": "text", "text": json!({ + "success": result.success, + "binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()), + "binary_size_bytes": result.binary_size, + "files_synced": result.files_synced, + "warnings": result.warnings.len(), + "errors": result.errors.len(), + "error_list": result.errors, + "warning_list": result.warnings, + }).to_string() }], + "isError": !result.success + })), + Err(e) => Ok(err_response( + serde_json::to_string(&json!({ "success": false, "error": format!("Compilation failed: {}", e) })).unwrap_or_default() + )), } } -pub async fn handle_search_indicators(config: &Config, args: &Value) -> Result { - let pattern = args.get("pattern") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; - - let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false); - let pattern_lower = pattern.to_lowercase(); - - let mut matches = Vec::new(); - - // Search custom indicators recursively - if let Some(indicators_dir) = &config.indicators_dir { - for entry in WalkDir::new(indicators_dir).max_depth(3) { - if let Ok(entry) = entry { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy().to_string(); - if name_str.to_lowercase().contains(&pattern_lower) { - let is_compiled = path.extension() - .map(|e| e == "ex5") - .unwrap_or(false); - matches.push(json!({ - "name": name_str, - "path": path.to_string_lossy().to_string(), - "type": "custom", - "compiled": is_compiled, - })); - } - } - } - } - } - - // Search built-in indicators if requested - if include_builtin { - let builtin = vec![ - "Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR", - "Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force", - "Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum", - "OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR", - ]; - for name in builtin { - if name.to_lowercase().contains(&pattern_lower) { - matches.push(json!({ - "name": name, - "path": null, - "type": "builtin", - "compiled": true, - })); - } - } - } - - matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "pattern": pattern, - "count": matches.len(), - "matches": matches, - }).to_string() }], - "isError": false - })) -} - -pub async fn handle_search_scripts(config: &Config, args: &Value) -> Result { - let pattern = args.get("pattern") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; - - let pattern_lower = pattern.to_lowercase(); - let mut matches = Vec::new(); - - if let Some(scripts_dir) = &config.scripts_dir { - for entry in WalkDir::new(scripts_dir).max_depth(3) { - if let Ok(entry) = entry { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy().to_string(); - if name_str.to_lowercase().contains(&pattern_lower) { - let is_compiled = path.extension() - .map(|e| e == "ex5") - .unwrap_or(false); - matches.push(json!({ - "name": name_str, - "path": path.to_string_lossy().to_string(), - "compiled": is_compiled, - })); - } - } - } - } - } - - matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "pattern": pattern, - "count": matches.len(), - "matches": matches, - }).to_string() }], - "isError": false - })) -} - pub async fn handle_copy_indicator_to_project(config: &Config, args: &Value) -> Result { - use std::path::PathBuf; - - let source_path = args.get("source_path") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("source_path is required"))?; - - let target_name = args.get("target_name").and_then(|v| v.as_str()); - - // Determine project directory - let project_dir = config.project_dir.as_ref() - .map(PathBuf::from) - .or_else(|| std::env::current_dir().ok()) - .ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?; - - let source = PathBuf::from(source_path); - if !source.exists() { - return Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": false, - "error": format!("Source file not found: {}", source_path), - }).to_string() }], - "isError": true - })); - } - - // Get extension - let ext = source.extension() - .and_then(|e| e.to_str()) - .unwrap_or("mq5"); - - // Determine target name - let target_filename = match target_name { - Some(name) => format!("{}.{}", name, ext), - None => source.file_name() - .and_then(|n| n.to_str()) - .map(|n| n.to_string()) - .unwrap_or_else(|| format!("indicator.{}", ext)), - }; - - let destination = project_dir.join(&target_filename); - - // Copy file - match std::fs::copy(&source, &destination) { - Ok(bytes) => { - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "source": source_path, - "destination": destination.to_string_lossy().to_string(), - "bytes_copied": bytes, - }).to_string() }], - "isError": false - })) - } - Err(e) => { - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": false, - "error": format!("Failed to copy file: {}", e), - }).to_string() }], - "isError": true - })) - } - } + copy_mql_to_project(config, args, "indicator") } pub async fn handle_copy_script_to_project(config: &Config, args: &Value) -> Result { - use std::path::PathBuf; - - let source_path = args.get("source_path") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("source_path is required"))?; - - let target_name = args.get("target_name").and_then(|v| v.as_str()); - - // Determine project directory - let project_dir = config.project_dir.as_ref() - .map(PathBuf::from) - .or_else(|| std::env::current_dir().ok()) - .ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?; - - let source = PathBuf::from(source_path); - if !source.exists() { - return Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": false, - "error": format!("Source file not found: {}", source_path), - }).to_string() }], - "isError": true - })); - } - - // Get extension - let ext = source.extension() - .and_then(|e| e.to_str()) - .unwrap_or("mq5"); - - // Determine target name - let target_filename = match target_name { - Some(name) => format!("{}.{}", name, ext), - None => source.file_name() - .and_then(|n| n.to_str()) - .map(|n| n.to_string()) - .unwrap_or_else(|| format!("script.{}", ext)), - }; - - let destination = project_dir.join(&target_filename); - - // Copy file - match std::fs::copy(&source, &destination) { - Ok(bytes) => { - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": true, - "source": source_path, - "destination": destination.to_string_lossy().to_string(), - "bytes_copied": bytes, - }).to_string() }], - "isError": false - })) - } - Err(e) => { - Ok(json!({ - "content": [{ "type": "text", "text": json!({ - "success": false, - "error": format!("Failed to copy file: {}", e), - }).to_string() }], - "isError": true - })) - } - } + copy_mql_to_project(config, args, "script") }