2026-04-19 08:19:45 +07:00
use anyhow ::{ Context , Result };
2026-04-19 08:11:17 +07:00
use serde_json ::{ json , Value };
use std ::fs ;
2026-04-19 08:19:45 +07:00
use std ::path ::{ Path , PathBuf };
2026-04-19 08:11:17 +07:00
use crate ::models ::Config ;
2026-04-19 08:19:45 +07:00
/// Validate that `user_path` resolves to a location within `allowed_base`.
/// Returns the canonicalized absolute path on success.
fn safe_output_path ( user_path : & str , allowed_base : & Path ) -> Result < PathBuf > {
// Resolve the base first (must already exist).
let base = allowed_base . canonicalize ()
. with_context ( || format! ( "allowed base directory does not exist: {} " , allowed_base . display ())) ? ;
// Build the candidate path. If the user supplied an absolute path we use it
// as-is; relative paths are joined onto the base so they stay inside it.
let candidate = {
let p = Path ::new ( user_path );
if p . is_absolute () { p . to_path_buf () } else { base . join ( p ) }
};
// Canonicalize the parent directory (the file itself need not exist yet).
let parent = candidate . parent ()
. ok_or_else ( || anyhow ::anyhow! ( "output_path has no parent directory" )) ? ;
let canonical_parent = parent . canonicalize ()
. with_context ( || format! ( "output_path parent directory does not exist: {} " , parent . display ())) ? ;
let canonical = canonical_parent . join (
candidate . file_name ()
. ok_or_else ( || anyhow ::anyhow! ( "output_path must include a filename" )) ? ,
);
if ! canonical . starts_with ( & base ) {
return Err ( anyhow ::anyhow! (
"output_path '{}' is outside the allowed directory '{}'" ,
user_path , base . display ()
));
}
Ok ( canonical )
}
2026-04-19 08:11:17 +07:00
/// Check if symbol has sufficient data for date range
pub async fn handle_check_symbol_data_status ( config : & Config , args : & Value ) -> Result < Value > {
let symbol = args . get ( "symbol" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "symbol is required" )) ? ;
let from_date = args . get ( "from_date" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "from_date is required" )) ? ;
let to_date = args . get ( "to_date" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "to_date is required" )) ? ;
// Get active account to determine which server to check
let current_account = config . current_account ();
let server = current_account . as_ref (). map ( | a | a . server . as_str ()). unwrap_or ( "" );
// Check if symbol exists in available symbols
let available_symbols = config . discover_symbols_for_active_account ();
let symbol_available = available_symbols . contains ( & symbol . to_string ());
if ! symbol_available {
return Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"symbol" : symbol ,
"has_sufficient_data" : false ,
"error" : format ! ( "Symbol '{}' not available for server '{}'" , symbol , server ),
"available_symbols" : available_symbols ,
"suggestion" : "Use get_active_account to see available symbols for this account"
}). to_string () }],
"isError" : false
}));
}
// Try to find hcc files to determine actual data range
let mt5_dir = config . mt5_dir ();
let mut data_range_start = None ;
let mut data_range_end = None ;
let mut bars_count = 0 ;
if let Some ( mt5_path ) = mt5_dir {
let bases_dir = mt5_path . join ( "Bases" );
if bases_dir . exists () {
// Look through servers for this symbol
for server_entry in fs ::read_dir ( & bases_dir ) ? . flatten () {
let server_name = server_entry . file_name (). to_string_lossy (). to_string ();
if server . is_empty () || server_name == server {
let symbol_dir = server_entry . path (). join ( "history" ). join ( symbol );
if symbol_dir . exists () {
// Count hcc files and get date range
for entry in fs ::read_dir ( & symbol_dir ) ? . flatten () {
if let Some ( ext ) = entry . path (). extension () {
if ext == "hcc" {
bars_count += 1 ;
if let Some ( fname ) = entry . file_name (). to_str () {
// Parse year from filename (e.g., "2024.hcc")
if let Ok ( year ) = fname . trim_end_matches ( ".hcc" ). parse ::< i32 > () {
if data_range_start . is_none () || year < data_range_start . unwrap () {
data_range_start = Some ( year );
}
if data_range_end . is_none () || year > data_range_end . unwrap () {
data_range_end = Some ( year );
}
}
}
}
}
}
}
}
}
}
}
// Parse requested date range
let parse_date = | date_str : & str | -> Option < ( i32 , i32 , i32 ) > {
let parts : Vec <& str > = date_str . split ( '.' ). collect ();
if parts . len () == 3 {
if let ( Ok ( y ), Ok ( m ), Ok ( d )) = ( parts [ 0 ]. parse (), parts [ 1 ]. parse (), parts [ 2 ]. parse ()) {
return Some (( y , m , d ));
}
}
None
};
let req_from = parse_date ( from_date );
let req_to = parse_date ( to_date );
let mut has_sufficient = true ;
let mut warnings = Vec ::new ();
if let ( Some ( req_year ), Some ( start_year ), Some ( end_year )) = ( req_from . map ( | d | d . 0 ), data_range_start , data_range_end ) {
if req_year < start_year {
has_sufficient = false ;
warnings . push ( format! ( "Requested start year {} is before available data start {} " , req_year , start_year ));
}
if let Some ( req_to_year ) = req_to . map ( | d | d . 0 ) {
if req_to_year > end_year {
has_sufficient = false ;
warnings . push ( format! ( "Requested end year {} is after available data end {} " , req_to_year , end_year ));
}
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"symbol" : symbol ,
"server" : server ,
"has_sufficient_data" : has_sufficient && bars_count > 0 ,
"requested_range" : {
"from" : from_date ,
"to" : to_date
},
"data_range" : match ( data_range_start , data_range_end ) {
( Some ( s ), Some ( e )) => format! ( " {} .01.01 - {} .12.31" , s , e ),
_ => "unknown" . to_string ()
},
"years_available" : data_range_end . map ( | e | e - data_range_start . unwrap_or ( e ) + 1 ). unwrap_or ( 0 ),
"hcc_files_count" : bars_count ,
"warnings" : if warnings . is_empty () { None } else { Some ( warnings ) },
"suggestion" : if ! has_sufficient { "Consider downloading more history in MT5 or adjusting date range" } else { "Data range is sufficient for backtest" }
}). to_string () }],
"isError" : false
}))
}
/// Get backtest history for EA/symbol
pub async fn handle_get_backtest_history ( config : & Config , args : & Value ) -> Result < Value > {
let expert_filter = args . get ( "expert" ). and_then ( | v | v . as_str ());
let symbol_filter = args . get ( "symbol" ). and_then ( | v | v . as_str ());
let limit = args . get ( "limit" ). and_then ( | v | v . as_u64 ()). unwrap_or ( 10 ) as usize ;
let reports_dir = config . reports_dir ();
let mut history = Vec ::new ();
if reports_dir . exists () {
for entry in fs ::read_dir ( & reports_dir ) ? . flatten () {
let path = entry . path ();
if path . is_dir () {
// Try to read metrics.json
let metrics_path = path . join ( "metrics.json" );
if let Ok ( content ) = fs ::read_to_string ( & metrics_path ) {
if let Ok ( metrics ) = serde_json ::from_str ::< Value > ( & content ) {
let report_expert = metrics . get ( "expert" ). and_then ( | v | v . as_str ());
let report_symbol = metrics . get ( "symbol" ). and_then ( | v | v . as_str ());
// Apply filters
if let ( Some ( e ), Some ( filter )) = ( report_expert , expert_filter ) {
if ! e . contains ( filter ) {
continue ;
}
}
if let ( Some ( s ), Some ( filter )) = ( report_symbol , symbol_filter ) {
if s != filter {
continue ;
}
}
// Extract key metrics
let summary = json! ({
"report_dir" : path . file_name (). and_then ( | n | n . to_str ()). unwrap_or ( "" ),
"date" : metrics . get ( "backtest_date" ). and_then ( | v | v . as_str ()),
"expert" : report_expert ,
"symbol" : report_symbol ,
"period" : metrics . get ( "period" ). and_then ( | v | v . as_str ()),
"profit" : metrics . get ( "net_profit" ). and_then ( | v | v . as_f64 ()),
"profit_factor" : metrics . get ( "profit_factor" ). and_then ( | v | v . as_f64 ()),
"expected_payoff" : metrics . get ( "expected_payoff" ). and_then ( | v | v . as_f64 ()),
"drawdown_pct" : metrics . get ( "drawdown_pct" ). and_then ( | v | v . as_f64 ()),
"total_trades" : metrics . get ( "total_trades" ). and_then ( | v | v . as_u64 ()),
"win_rate" : metrics . get ( "win_rate" ). and_then ( | v | v . as_f64 ()),
});
history . push ( summary );
}
}
}
}
}
// Sort by date (newest first)
history . sort_by ( | a , b | {
let date_a = a . get ( "date" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
let date_b = b . get ( "date" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
date_b . cmp ( date_a )
});
// Apply limit
let total = history . len ();
let limited : Vec < Value > = history . into_iter (). take ( limit ). collect ();
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : true ,
"count" : limited . len (),
"total" : total ,
"filters" : {
"expert" : expert_filter ,
"symbol" : symbol_filter
},
"history" : limited ,
"hint" : if limited . is_empty () { "No backtest history found. Run a backtest first with run_backtest." } else { "Use compare_backtests to analyze differences between results." }
}). to_string () }],
"isError" : false
}))
}
/// Compare multiple backtests
2026-04-19 08:19:45 +07:00
pub async fn handle_compare_backtests ( _config : & Config , args : & Value ) -> Result < Value > {
2026-04-19 08:11:17 +07:00
let report_dirs = args . get ( "report_dirs" )
. and_then ( | v | v . as_array ())
. ok_or_else ( || anyhow ::anyhow! ( "report_dirs array is required" )) ? ;
if report_dirs . len () < 2 {
return Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"error" : "At least 2 report directories required for comparison"
}). to_string () }],
"isError" : true
}));
}
let mut comparisons = Vec ::new ();
let mut base_metrics : Option < Value > = None ;
for dir_value in report_dirs {
let dir = dir_value . as_str (). unwrap_or ( "" );
let path = Path ::new ( dir );
let metrics_path = path . join ( "metrics.json" );
if let Ok ( content ) = fs ::read_to_string ( & metrics_path ) {
if let Ok ( metrics ) = serde_json ::from_str ::< Value > ( & content ) {
if base_metrics . is_none () {
base_metrics = Some ( metrics . clone ());
}
let summary = json! ({
"report_dir" : dir ,
"expert" : metrics . get ( "expert" ). and_then ( | v | v . as_str ()),
"symbol" : metrics . get ( "symbol" ). and_then ( | v | v . as_str ()),
"net_profit" : metrics . get ( "net_profit" ). and_then ( | v | v . as_f64 ()),
"profit_factor" : metrics . get ( "profit_factor" ). and_then ( | v | v . as_f64 ()),
"drawdown_pct" : metrics . get ( "drawdown_pct" ). and_then ( | v | v . as_f64 ()),
"total_trades" : metrics . get ( "total_trades" ). and_then ( | v | v . as_u64 ()),
"win_rate" : metrics . get ( "win_rate" ). and_then ( | v | v . as_f64 ()),
"expected_payoff" : metrics . get ( "expected_payoff" ). and_then ( | v | v . as_f64 ()),
"recovery_factor" : metrics . get ( "recovery_factor" ). and_then ( | v | v . as_f64 ()),
"sharpe_ratio" : metrics . get ( "sharpe_ratio" ). and_then ( | v | v . as_f64 ()),
});
comparisons . push ( summary );
}
}
}
// Calculate differences if we have base
let mut analysis = Vec ::new ();
if let Some ( base ) = & base_metrics {
let base_profit = base . get ( "net_profit" ). and_then ( | v | v . as_f64 ()). unwrap_or ( 0.0 );
let base_dd = base . get ( "drawdown_pct" ). and_then ( | v | v . as_f64 ()). unwrap_or ( 0.0 );
let base_pf = base . get ( "profit_factor" ). and_then ( | v | v . as_f64 ()). unwrap_or ( 0.0 );
2026-04-19 08:19:45 +07:00
for ( _i , comp ) in comparisons . iter (). enumerate (). skip ( 1 ) {
2026-04-19 08:11:17 +07:00
let profit = comp . get ( "net_profit" ). and_then ( | v | v . as_f64 ()). unwrap_or ( 0.0 );
let dd = comp . get ( "drawdown_pct" ). and_then ( | v | v . as_f64 ()). unwrap_or ( 0.0 );
let pf = comp . get ( "profit_factor" ). and_then ( | v | v . as_f64 ()). unwrap_or ( 0.0 );
analysis . push ( json! ({
"compare_to" : comparisons [ 0 ]. get ( "report_dir" ),
"report" : comp . get ( "report_dir" ),
"profit_diff" : profit - base_profit ,
"profit_pct_change" : if base_profit != 0.0 { (( profit - base_profit ) / base_profit . abs ()) * 100.0 } else { 0.0 },
"drawdown_diff" : dd - base_dd ,
"profit_factor_diff" : pf - base_pf ,
"verdict" : if profit > base_profit && dd <= base_dd { "better" }
else if profit < base_profit { "worse" }
else { "mixed" }
}));
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : true ,
"count" : comparisons . len (),
"comparisons" : comparisons ,
"analysis" : if analysis . is_empty () { None } else { Some ( analysis ) },
"verdict" : if comparisons . len () >= 2 {
let profits : Vec < f64 > = comparisons . iter ()
. filter_map ( | c | c . get ( "net_profit" ). and_then ( | v | v . as_f64 ()))
. collect ();
let best_idx = profits . iter (). enumerate ()
. max_by ( | a , b | a . 1. partial_cmp ( b . 1 ). unwrap_or ( std ::cmp ::Ordering ::Equal ))
. map ( | ( i , _ ) | i );
best_idx . map ( | i | format! ( "Best: {} " ,
comparisons . get ( i ). and_then ( | c | c . get ( "report_dir" ). and_then ( | v | v . as_str ())). unwrap_or ( "unknown" )))
} else { None }
}). to_string () }],
"isError" : false
}))
}
/// Initialize new MQL5 project
pub async fn handle_init_project ( config : & Config , args : & Value ) -> Result < Value > {
let name = args . get ( "name" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "name is required" )) ? ;
let template = args . get ( "template" ). and_then ( | v | v . as_str ()). unwrap_or ( "basic" );
// Determine project directory
let project_dir = config . project_dir . as_ref ()
. map ( | p | Path ::new ( p ). to_path_buf ())
. or_else ( || std ::env ::current_dir (). ok ())
. ok_or_else ( || anyhow ::anyhow! ( "Cannot determine project directory" )) ? ;
let ea_path = project_dir . join ( format! ( " {} .mq5" , name ));
// Basic EA template
let ea_template = match template {
"scalper" => format! ( r #"//+------------------------------------------------------------------+
//| {}.mq5 |
//| Scalper EA Template |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link ""
#property version "1.00"
#property strict
input double Lots = 0.1;
input int StopLoss = 20; // points
input int TakeProfit = 10; // points
input int Slippage = 3;
int OnInit() {{
Print("{} Scalper EA initialized");
return(INIT_SUCCEEDED);
}}
void OnDeinit(const int reason) {{
Print("{} EA stopped, reason: ", reason);
}}
void OnTick() {{
static datetime last_bar = 0;
datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);
if(current_bar == last_bar) return; // New bar only
last_bar = current_bar;
// Fast MAs crossover logic here
double ma_fast = iMA(_Symbol, PERIOD_CURRENT, 5, 0, MODE_EMA, PRICE_CLOSE, 0);
double ma_slow = iMA(_Symbol, PERIOD_CURRENT, 15, 0, MODE_EMA, PRICE_CLOSE, 0);
if(PositionsTotal() == 0) {{
if(ma_fast > ma_slow) {{
// Buy signal
// OrderSend(_Symbol, ORDER_TYPE_BUY, Lots, Ask, Slippage, Bid-StopLoss*_Point, Bid+TakeProfit*_Point);
}}
else if(ma_fast < ma_slow) {{
// Sell signal
// OrderSend(_Symbol, ORDER_TYPE_SELL, Lots, Bid, Slippage, Ask+StopLoss*_Point, Ask-TakeProfit*_Point);
}}
}}
}}
"# , name , name , name ),
"swing" => format! ( r #"//+------------------------------------------------------------------+
//| {}.mq5 |
//| Swing EA Template |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link ""
#property version "1.00"
#property strict
input double Lots = 0.1;
input int StopLoss = 100; // points
input int TakeProfit = 200; // points
input int TrailingStop = 50; // points
int OnInit() {{
Print("{} Swing EA initialized");
return(INIT_SUCCEEDED);
}}
void OnDeinit(const int reason) {{
Print("{} EA stopped");
}}
void OnTick() {{
// Daily/H4 trend following logic
double trend_ma = iMA(_Symbol, PERIOD_D1, 50, 0, MODE_SMA, PRICE_CLOSE, 0);
double price = iClose(_Symbol, PERIOD_CURRENT, 0);
// Implementation here
}}
"# , name , name , name ),
"grid" => format! ( r #"//+------------------------------------------------------------------+
//| {}.mq5 |
//| Grid EA Template |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link ""
#property version "1.00"
#property strict
input double StartLots = 0.01;
input double LotMultiplier = 1.5;
input int GridPips = 20;
input int MaxLevels = 10;
int OnInit() {{
Print("{} Grid EA initialized - Use with extreme caution!");
return(INIT_SUCCEEDED);
}}
void OnDeinit(const int reason) {{
Print("{} Grid EA stopped");
}}
void OnTick() {{
// Grid trading logic
// WARNING: Grid strategies can lead to significant losses
}}
"# , name , name , name ),
_ => format! ( r #"//+------------------------------------------------------------------+
//| {}.mq5 |
//| Basic EA Template |
//+------------------------------------------------------------------+
#property copyright "Your Name"
#property link ""
#property version "1.00"
#property strict
input double Lots = 0.1;
input int StopLoss = 50;
input int TakeProfit = 50;
int OnInit() {{
Print("{} EA initialized");
return(INIT_SUCCEEDED);
}}
void OnDeinit(const int reason) {{
Print("{} EA stopped");
}}
void OnTick() {{
// Your trading logic here
// Check for open positions, signals, etc.
}}
"# , name , name , name )
};
// Write EA file
fs ::write ( & ea_path , ea_template ) ? ;
// Create README
let readme_path = project_dir . join ( "README.md" );
let readme_content = format! ( r #"# {}
MQL5 Expert Advisor generated by MT5-Quant
## Files
- `{}`.mq5 - Main EA source code
## Parameters
Edit the `input` variables in the source code to customize:
- `Lots` - Trading lot size
- `StopLoss` - Stop loss in points
- `TakeProfit` - Take profit in points
## Build & Test
```bash
# Compile EA
mt5-quant compile_ea expert={}
# Run backtest
mt5-quant run_backtest expert={} symbol=XAUUSDc
```
"# , name , name , name , name );
fs ::write ( & readme_path , readme_content ) ? ;
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : true ,
"project_name" : name ,
"template" : template ,
"created_files" : [
ea_path . to_string_lossy (). to_string (),
readme_path . to_string_lossy (). to_string ()
],
"hint" : format ! ( "Edit {}.mq5 to implement your strategy, then compile with compile_ea" , name )
}). to_string () }],
"isError" : false
}))
}
/// Validate EA syntax (basic check without full compile)
pub async fn handle_validate_ea_syntax ( _config : & Config , args : & Value ) -> Result < Value > {
let path = args . get ( "path" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "path is required" )) ? ;
let content = fs ::read_to_string ( path ) ? ;
let mut errors = Vec ::new ();
let mut warnings = Vec ::new ();
// Basic syntax checks
let lines : Vec <& str > = content . lines (). collect ();
for ( i , line ) in lines . iter (). enumerate () {
let line_num = i + 1 ;
let trimmed = line . trim ();
// Check for basic issues
if trimmed . ends_with ( '{' ) && ! trimmed . contains ( '}' ) {
// This is just a heuristic, not a real syntax error
}
if trimmed . contains ( "OrderSend" ) && ! trimmed . starts_with ( "//" ) {
warnings . push ( json! ({
"line" : line_num ,
"message" : "OrderSend found - ensure proper error handling" ,
"severity" : "warning"
}));
}
if trimmed . contains ( "input" ) && trimmed . contains ( "double" ) && trimmed . contains ( "Lots" ) {
if ! trimmed . contains ( "=" ) {
warnings . push ( json! ({
"line" : line_num ,
"message" : "Input parameter 'Lots' has no default value" ,
"severity" : "warning"
}));
}
}
}
// Check for required sections
let has_on_init = content . contains ( "int OnInit()" );
let has_on_tick = content . contains ( "void OnTick()" );
let has_on_deinit = content . contains ( "void OnDeinit" );
if ! has_on_init {
errors . push ( json! ({
"line" : 0 ,
"message" : "Missing OnInit() function - required for EA" ,
"severity" : "error"
}));
}
if ! has_on_tick && ! content . contains ( "void OnTimer()" ) {
warnings . push ( json! ({
"line" : 0 ,
"message" : "No OnTick() or OnTimer() found - EA won't respond to events" ,
"severity" : "warning"
}));
}
let valid = errors . is_empty ();
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : valid ,
"valid" : valid ,
"path" : path ,
"checks" : {
"has_on_init" : has_on_init ,
"has_on_tick" : has_on_tick ,
"has_on_deinit" : has_on_deinit ,
"lines" : lines . len ()
},
"errors" : if errors . is_empty () { None } else { Some ( errors ) },
"warnings" : if warnings . is_empty () { None } else { Some ( warnings ) },
"hint" : if valid { "Syntax looks good. Run compile_ea for full compilation check." } else { "Fix errors before compiling." }
}). to_string () }],
"isError" : ! valid
}))
}
/// Check MT5 terminal status
pub async fn handle_check_mt5_status ( config : & Config ) -> Result < Value > {
let mt5_dir = config . mt5_dir ();
let wine_exe = config . wine_executable . as_ref ();
// Check if MT5 files exist
let terminal_exists = mt5_dir . as_ref (). map ( | d | d . join ( "terminal64.exe" ). exists ()). unwrap_or ( false );
let metaeditor_exists = mt5_dir . as_ref (). map ( | d | d . join ( "metaeditor64.exe" ). exists ()). unwrap_or ( false );
let tester_exists = mt5_dir . as_ref (). map ( | d | d . join ( "metatester64.exe" ). exists ()). unwrap_or ( false );
// Check Wine
let wine_ok = wine_exe . map ( | w | Path ::new ( w ). exists ()). unwrap_or ( false );
// Try to get MT5 version (would need to actually run it, skip for now)
let mut mt5_version = None ;
if wine_ok && terminal_exists {
// Could run: wine terminal64.exe /version but it's complex
mt5_version = Some ( "detected" . to_string ());
}
let all_ok = terminal_exists && metaeditor_exists && tester_exists && wine_ok ;
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : all_ok ,
"terminal_ready" : all_ok ,
"checks" : {
"mt5_dir_exists" : mt5_dir . as_ref (). map ( | d | d . exists ()). unwrap_or ( false ),
"terminal64_exe" : terminal_exists ,
"metaeditor64_exe" : metaeditor_exists ,
"metatester64_exe" : tester_exists ,
"wine_executable" : wine_ok ,
"wine_path" : wine_exe
},
"mt5_version" : mt5_version ,
"current_account" : config . current_account (). map ( | a | json! ({
"login" : a . login ,
"server" : a . server
})),
"hint" : if all_ok {
"MT5 is ready for backtesting and optimization."
} else {
"Some components missing. Run verify_setup for detailed diagnostics."
}
}). to_string () }],
"isError" : false
}))
}
/// Create .set file template from EA
pub async fn handle_create_set_template ( config : & Config , args : & Value ) -> Result < Value > {
let ea = args . get ( "ea" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "ea is required" )) ? ;
let output_path = args . get ( "output_path" ). and_then ( | v | v . as_str ());
// Find EA source file
let ea_path = if Path ::new ( ea ). exists () {
Path ::new ( ea ). to_path_buf ()
} else if let Some ( experts_dir ) = & config . experts_dir {
Path ::new ( experts_dir ). join ( format! ( " {} .mq5" , ea ))
} else {
return Err ( anyhow ::anyhow! ( "Cannot find EA: {}" , ea ));
};
if ! ea_path . exists () {
return Err ( anyhow ::anyhow! ( "EA file not found: {}" , ea_path . display ()));
}
let content = fs ::read_to_string ( & ea_path ) ? ;
let mut inputs = Vec ::new ();
// Parse input declarations
for line in content . lines () {
let trimmed = line . trim ();
if trimmed . starts_with ( "input " ) {
// Parse: input type name = default; // comment
let without_input = & trimmed [ 6 .. ]; // Remove "input "
// Extract comment if any
let parts : Vec <& str > = without_input . split ( "//" ). collect ();
let decl = parts [ 0 ]. trim ();
let comment = parts . get ( 1 ). map ( | c | c . trim ());
// Parse type and name
let tokens : Vec <& str > = decl . split_whitespace (). collect ();
if tokens . len () >= 2 {
let type_name = tokens [ 0 ];
let rest = tokens [ 1 .. ]. to_vec (). join ( " " );
// Parse name = value
let name_value : Vec <& str > = rest . split ( '=' ). collect ();
let name = name_value [ 0 ]. trim ();
let default_val = name_value . get ( 1 ). map ( | v | v . trim (). trim_end_matches ( ';' )). unwrap_or ( "0" );
inputs . push ( json! ({
"name" : name ,
"type" : type_name ,
"default" : default_val ,
"description" : comment
}));
}
}
}
// Generate .set content
let mut set_content = format! ( "; {} parameters generated by MT5-Quant \n " , ea );
set_content . push_str ( "; Format: name=value \n\n " );
for input in & inputs {
let name = input . get ( "name" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
let default = input . get ( "default" ). and_then ( | v | v . as_str ()). unwrap_or ( "0" );
let desc = input . get ( "description" ). and_then ( | v | v . as_str ()). unwrap_or ( "" );
if ! desc . is_empty () {
set_content . push_str ( & format! ( "; {} \n " , desc ));
}
set_content . push_str ( & format! ( " {} = {} \n\n " , name , default ));
}
// Determine output path
let set_path = if let Some ( path ) = output_path {
2026-04-19 08:19:45 +07:00
// Restrict writes to the tester profiles dir (or the EA's own dir as fallback).
let allowed_base = if let Some ( profiles_dir ) = & config . tester_profiles_dir {
Path ::new ( profiles_dir ). to_path_buf ()
} else {
ea_path . parent (). unwrap_or ( Path ::new ( "." )). to_path_buf ()
};
safe_output_path ( path , & allowed_base ) ?
2026-04-19 08:11:17 +07:00
} else if let Some ( profiles_dir ) = & config . tester_profiles_dir {
Path ::new ( profiles_dir ). join ( format! ( " {} .set" , ea ))
} else {
ea_path . with_extension ( "set" )
};
fs ::write ( & set_path , set_content ) ? ;
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : true ,
"ea" : ea ,
"inputs_found" : inputs . len (),
"inputs" : inputs ,
"set_file" : set_path . to_string_lossy (). to_string (),
"hint" : "Edit .set file to modify parameter values, then use with run_backtest set_file=..."
}). to_string () }],
"isError" : false
}))
}
/// Export backtest report to various formats
pub async fn handle_export_report ( _config : & Config , args : & Value ) -> Result < Value > {
let report_dir = args . get ( "report_dir" )
. and_then ( | v | v . as_str ())
. ok_or_else ( || anyhow ::anyhow! ( "report_dir is required" )) ? ;
let format = args . get ( "format" ). and_then ( | v | v . as_str ()). unwrap_or ( "csv" );
let output_path = args . get ( "output_path" ). and_then ( | v | v . as_str ());
let path = Path ::new ( report_dir );
let metrics_path = path . join ( "metrics.json" );
2026-04-19 08:19:45 +07:00
let _deals_path = path . join ( "deals.csv" );
2026-04-19 08:11:17 +07:00
// Read metrics
let metrics : Value = if metrics_path . exists () {
fs ::read_to_string ( & metrics_path )
. ok ()
. and_then ( | c | serde_json ::from_str ( & c ). ok ())
. unwrap_or ( json! ({}))
} else {
json! ({})
};
2026-04-19 08:19:45 +07:00
// Determine output file; restrict user-supplied path to the report directory.
2026-04-19 08:11:17 +07:00
let output = match output_path {
2026-04-19 08:19:45 +07:00
Some ( p ) => safe_output_path ( p , path ) ? ,
None => path . join ( format! ( "report. {} " , format )),
2026-04-19 08:11:17 +07:00
};
let content = match format {
"csv" => {
// Simple CSV export of metrics
let mut csv = "Metric,Value \n " . to_string ();
if let Some ( obj ) = metrics . as_object () {
for ( key , value ) in obj {
csv . push_str ( & format! ( " {} , {} \n " , key , value ));
}
}
csv
}
"md" => {
// Markdown format
let mut md = format! ( "# Backtest Report: {} \n\n " , metrics . get ( "expert" ). and_then ( | v | v . as_str ()). unwrap_or ( "Unknown" ));
md . push_str ( "## Summary \n\n " );
if let Some ( obj ) = metrics . as_object () {
for ( key , value ) in obj {
md . push_str ( & format! ( "- ** {} **: {} \n " , key , value ));
}
}
md
}
_ => metrics . to_string () // JSON fallback
};
fs ::write ( & output , content ) ? ;
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : json ! ({
"success" : true ,
"format" : format ,
"output_file" : output . to_string_lossy (). to_string (),
"source" : report_dir ,
"hint" : "Exported report ready for analysis or sharing."
}). to_string () }],
"isError" : false
}))
}
2026-04-20 02:25:07 +07:00
// === Wine & MT5 Debugging Tools ===
/// Diagnose Wine installation and prefix health
pub async fn handle_diagnose_wine ( config : & Config , _args : & Value ) -> Result < Value > {
let mut diagnostics = json! ({
"wine_executable" : null ,
"wine_version" : null ,
"wine_prefix" : null ,
"prefix_health" : null ,
"prefix_exists" : false ,
"prefix_size_mb" : 0 ,
"errors" : Vec ::< String > ::new (),
"warnings" : Vec ::< String > ::new (),
});
// Check wine executable
if let Some ( wine_exe ) = config . wine_executable . as_ref () {
diagnostics [ "wine_executable" ] = json! ( wine_exe );
// Get Wine version
let version_output = std ::process ::Command ::new ( wine_exe )
. arg ( "--version" )
. output ();
match version_output {
Ok ( output ) if output . status . success () => {
let version = String ::from_utf8_lossy ( & output . stdout ). trim (). to_string ();
diagnostics [ "wine_version" ] = json! ( version );
}
_ => {
diagnostics [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "Failed to get Wine version - Wine may not be properly installed" )
);
}
}
} else {
diagnostics [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "Wine executable not configured" )
);
}
// Check Wine prefix
if let Some ( mt5_dir ) = config . mt5_dir () {
let wine_prefix = mt5_dir
. parent ()
. and_then ( | p | p . parent ())
. and_then ( | p | p . parent ());
if let Some ( prefix ) = wine_prefix {
diagnostics [ "wine_prefix" ] = json! ( prefix . to_string_lossy (). to_string ());
// Check if prefix exists
let prefix_exists = prefix . exists ();
diagnostics [ "prefix_exists" ] = json! ( prefix_exists );
if prefix_exists {
// Calculate prefix size
let mut total_size = 0 u64 ;
fn calculate_size ( dir : & Path , total : & mut u64 ) {
if let Ok ( entries ) = fs ::read_dir ( dir ) {
for entry in entries . flatten () {
let path = entry . path ();
if path . is_file () {
if let Ok ( meta ) = entry . metadata () {
* total += meta . len ();
}
} else if path . is_dir () {
calculate_size ( & path , total );
}
}
}
}
calculate_size ( prefix , & mut total_size );
diagnostics [ "prefix_size_mb" ] = json! (( total_size / 1024 / 1024 ) as i64 );
// Check critical directories
let system32 = prefix . join ( "drive_c/windows/system32" );
let program_files = prefix . join ( "drive_c/Program Files" );
if ! system32 . exists () {
diagnostics [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "Wine prefix missing system32 directory - prefix may be corrupted" )
);
diagnostics [ "prefix_health" ] = json! ( "corrupted" );
} else if ! program_files . exists () {
diagnostics [ "warnings" ]. as_array_mut (). unwrap (). push (
json! ( "Program Files directory not found" )
);
diagnostics [ "prefix_health" ] = json! ( "incomplete" );
} else {
diagnostics [ "prefix_health" ] = json! ( "healthy" );
}
// Check for recent Wine errors
let wine_log = prefix . join ( "wine.log" );
if wine_log . exists () {
if let Ok ( content ) = fs ::read_to_string ( & wine_log ) {
let recent_errors : Vec <& str > = content . lines ()
. filter ( | l | l . contains ( "err:" ) || l . contains ( "fixme:" ))
. rev ()
. take ( 10 )
. collect ();
if ! recent_errors . is_empty () {
diagnostics [ "recent_wine_errors" ] = json! ( recent_errors );
}
}
}
} else {
diagnostics [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "Wine prefix directory does not exist" )
);
diagnostics [ "prefix_health" ] = json! ( "missing" );
}
} else {
diagnostics [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "Could not determine Wine prefix from MT5 directory" )
);
}
} else {
diagnostics [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "MT5 directory not configured" )
);
}
let has_errors = ! diagnostics [ "errors" ]. as_array (). unwrap (). is_empty ();
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : diagnostics . to_string () }],
"isError" : has_errors
}))
}
/// Get MT5 terminal logs
pub async fn handle_get_mt5_logs ( config : & Config , args : & Value ) -> Result < Value > {
let log_type = args . get ( "log_type" ). and_then ( | v | v . as_str ()). unwrap_or ( "terminal" );
let lines = args . get ( "lines" ). and_then ( | v | v . as_u64 ()). unwrap_or ( 100 ) as usize ;
let search = args . get ( "search" ). and_then ( | v | v . as_str ());
let mt5_dir = config . mt5_dir ()
. ok_or_else ( || anyhow ::anyhow! ( "MT5 directory not configured" )) ? ;
let log_path = match log_type {
"terminal" => mt5_dir . join ( "logs" ). join ( format! ( " {} " , chrono ::Local ::now (). format ( "%Y%m%d" ))),
"tester" => mt5_dir . join ( "Tester" ). join ( "logs" ),
"metaeditor" => mt5_dir . join ( "MetaEditor" ). join ( "logs" ),
_ => mt5_dir . join ( "logs" ),
};
let mut result = json! ({
"log_type" : log_type ,
"log_path" : log_path . to_string_lossy (). to_string (),
"found" : false ,
"lines_total" : 0 ,
"lines_returned" : 0 ,
"content" : Vec ::< String > ::new (),
});
// Find log files
let mut log_files : Vec < _ > = Vec ::new ();
if log_path . exists () {
if let Ok ( entries ) = fs ::read_dir ( & log_path ) {
for entry in entries . flatten () {
let path = entry . path ();
if path . is_file () {
if let Some ( ext ) = path . extension () {
if ext == "log" {
if let Ok ( meta ) = entry . metadata () {
if let Ok ( modified ) = meta . modified () {
log_files . push (( path , modified ));
}
}
}
}
}
}
}
}
// Sort by modification time (newest first)
log_files . sort_by ( | a , b | b . 1. cmp ( & a . 1 ));
if let Some (( latest_log , _ )) = log_files . first () {
result [ "found" ] = json! ( true );
if let Ok ( content ) = fs ::read_to_string ( latest_log ) {
let all_lines : Vec <& str > = content . lines (). collect ();
result [ "lines_total" ] = json! ( all_lines . len ());
// Filter and limit lines
let mut filtered : Vec <& str > = all_lines . clone ();
// Apply search filter
if let Some ( search_term ) = search {
let search_lower = search_term . to_lowercase ();
filtered . retain ( | line | line . to_lowercase (). contains ( & search_lower ));
}
// Get last N lines
let start = filtered . len (). saturating_sub ( lines );
let final_lines : Vec < String > = filtered [ start .. ]. iter (). map ( | s | s . to_string ()). collect ();
result [ "lines_returned" ] = json! ( final_lines . len ());
result [ "content" ] = json! ( final_lines );
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : false
}))
}
/// Search MT5 logs for error patterns
pub async fn handle_search_mt5_errors ( config : & Config , args : & Value ) -> Result < Value > {
let error_patterns = vec! [
"error" , "failed" , "crash" , "exception" , "access violation" ,
"out of memory" , "cannot" , "unable to" , "terminated"
];
let hours_back = args . get ( "hours_back" ). and_then ( | v | v . as_u64 ()). unwrap_or ( 24 );
let max_errors = args . get ( "max_errors" ). and_then ( | v | v . as_u64 ()). unwrap_or ( 50 ) as usize ;
let mt5_dir = config . mt5_dir ()
. ok_or_else ( || anyhow ::anyhow! ( "MT5 directory not configured" )) ? ;
let mut errors_found = Vec ::new ();
let logs_dir = mt5_dir . join ( "logs" );
let cutoff_time = std ::time ::SystemTime ::now () - std ::time ::Duration ::from_secs ( hours_back * 3600 );
// Search recent log files
if logs_dir . exists () {
if let Ok ( entries ) = fs ::read_dir ( & logs_dir ) {
for entry in entries . flatten () {
let path = entry . path ();
if path . extension (). map ( | e | e == "log" ). unwrap_or ( false ) {
if let Ok ( meta ) = entry . metadata () {
if let Ok ( modified ) = meta . modified () {
if modified >= cutoff_time {
if let Ok ( content ) = fs ::read_to_string ( & path ) {
for ( i , line ) in content . lines (). enumerate () {
let line_lower = line . to_lowercase ();
for pattern in & error_patterns {
if line_lower . contains ( pattern ) {
errors_found . push ( json! ({
"file" : path . file_name (). unwrap_or_default (). to_string_lossy (). to_string (),
"line" : i + 1 ,
"content" : line . trim (). to_string (),
"pattern" : pattern ,
}));
if errors_found . len () >= max_errors {
break ;
}
}
}
}
}
}
}
}
}
if errors_found . len () >= max_errors {
break ;
}
}
}
}
let result = json! ({
"hours_searched" : hours_back ,
"errors_found" : errors_found . len (),
"max_errors" : max_errors ,
"errors" : errors_found ,
"suggestion" : if errors_found . is_empty () {
"No errors found in recent logs. Check get_mt5_logs for full log content."
} else {
"Found potential errors. Review the 'content' field for details."
},
});
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : false
}))
}
/// Check MT5 process status
pub async fn handle_check_mt5_process ( _config : & Config , _args : & Value ) -> Result < Value > {
use std ::process ::Command ;
let mut result = json! ({
"is_running" : false ,
"processes" : Vec ::< serde_json ::Value > ::new (),
"wine_server_running" : false ,
"total_instances" : 0 ,
});
#[cfg(target_os = "macos" )]
{
// Check for MT5 processes
let ps_output = Command ::new ( "ps" )
. args ([ "aux" ])
. output ();
if let Ok ( output ) = ps_output {
let content = String ::from_utf8_lossy ( & output . stdout );
let mut processes = Vec ::new ();
let mut mt5_count = 0 ;
let mut wine_server = false ;
for line in content . lines () {
let line_lower = line . to_lowercase ();
if line_lower . contains ( "terminal64" ) || line_lower . contains ( "metatrader" ) {
let parts : Vec <& str > = line . split_whitespace (). collect ();
if parts . len () >= 11 {
processes . push ( json! ({
"pid" : parts [ 1 ],
"cpu" : parts [ 2 ],
"mem" : parts [ 3 ],
"command" : parts [ 10 .. ]. join ( " " ),
}));
mt5_count += 1 ;
}
}
if line_lower . contains ( "wineserver" ) {
wine_server = true ;
}
}
result [ "processes" ] = json! ( processes );
result [ "is_running" ] = json! ( mt5_count > 0 );
result [ "total_instances" ] = json! ( mt5_count );
result [ "wine_server_running" ] = json! ( wine_server );
}
}
#[cfg(target_os = "linux" )]
{
let ps_output = Command ::new ( "ps" )
. args ([ "aux" ])
. output ();
if let Ok ( output ) = ps_output {
let content = String ::from_utf8_lossy ( & output . stdout );
let mut processes = Vec ::new ();
let mut mt5_count = 0 ;
let mut wine_server = false ;
for line in content . lines () {
let line_lower = line . to_lowercase ();
if line_lower . contains ( "terminal64" ) || line_lower . contains ( "metatrader" ) {
let parts : Vec <& str > = line . split_whitespace (). collect ();
if parts . len () >= 11 {
processes . push ( json! ({
"pid" : parts [ 1 ],
"cpu" : parts [ 2 ],
"mem" : parts [ 3 ],
"command" : parts [ 10 .. ]. join ( " " ),
}));
mt5_count += 1 ;
}
}
if line_lower . contains ( "wineserver" ) {
wine_server = true ;
}
}
result [ "processes" ] = json! ( processes );
result [ "is_running" ] = json! ( mt5_count > 0 );
result [ "total_instances" ] = json! ( mt5_count );
result [ "wine_server_running" ] = json! ( wine_server );
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : false
}))
}
/// Kill stuck MT5 process
pub async fn handle_kill_mt5_process ( _config : & Config , args : & Value ) -> Result < Value > {
use std ::process ::Command ;
let force = args . get ( "force" ). and_then ( | v | v . as_bool ()). unwrap_or ( false );
let pid = args . get ( "pid" ). and_then ( | v | v . as_str ());
let mut killed = Vec ::new ();
let mut failed = Vec ::new ();
#[cfg(any(target_os = "macos" , target_os = "linux" ))]
{
// Get list of MT5 processes
let ps_output = Command ::new ( "ps" )
. args ([ "aux" ])
. output ();
if let Ok ( output ) = ps_output {
let content = String ::from_utf8_lossy ( & output . stdout );
for line in content . lines () {
let line_lower = line . to_lowercase ();
let should_kill = if let Some ( target_pid ) = pid {
line . contains ( target_pid ) && ( line_lower . contains ( "terminal64" ) || line_lower . contains ( "metatrader" ))
} else {
line_lower . contains ( "terminal64" ) || line_lower . contains ( "metatrader" )
};
if should_kill {
let parts : Vec <& str > = line . split_whitespace (). collect ();
if parts . len () >= 2 {
let process_pid = parts [ 1 ];
let signal = if force { "-9" } else { "-15" };
match Command ::new ( "kill" ). args ([ signal , process_pid ]). output () {
Ok ( _ ) => killed . push ( process_pid . to_string ()),
Err ( e ) => failed . push ( format! ( " {} : {} " , process_pid , e )),
}
}
}
}
}
// Also kill wineserver if force=true
if force {
let _ = Command ::new ( "killall" ). arg ( "wineserver" ). output ();
}
}
let message = if killed . is_empty () {
"No MT5 processes found to kill" . to_string ()
} else {
format! ( "Killed {} MT5 process(es)" , killed . len ())
};
let result = json! ({
"killed" : killed ,
"failed" : failed ,
"force" : force ,
"message" : message ,
});
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : ! failed . is_empty ()
}))
}
/// Check system resources for MT5
pub async fn handle_check_system_resources ( _config : & Config , _args : & Value ) -> Result < Value > {
use std ::process ::Command ;
let mut result = json! ({
"disk_space" : null ,
"memory" : null ,
"cpu_cores" : 0 ,
"recommendations" : Vec ::< String > ::new (),
});
#[cfg(any(target_os = "macos" , target_os = "linux" ))]
{
// Check disk space
let df_output = Command ::new ( "df" )
. args ([ "-h" , "/" ])
. output ();
if let Ok ( output ) = df_output {
let content = String ::from_utf8_lossy ( & output . stdout );
for line in content . lines (). skip ( 1 ) {
let parts : Vec <& str > = line . split_whitespace (). collect ();
if parts . len () >= 5 {
result [ "disk_space" ] = json! ({
"filesystem" : parts [ 0 ],
"size" : parts [ 1 ],
"used" : parts [ 2 ],
"available" : parts [ 3 ],
"use_percent" : parts [ 4 ],
});
// Check if low on space
let use_pct = parts [ 4 ]. trim_end_matches ( '%' ). parse ::< u32 > (). unwrap_or ( 0 );
if use_pct > 90 {
result [ "recommendations" ]. as_array_mut (). unwrap (). push (
json! ( "Disk space critically low. Clean MT5 cache with clean_cache." )
);
} else if use_pct > 80 {
result [ "recommendations" ]. as_array_mut (). unwrap (). push (
json! ( "Disk space getting low. Consider cleaning cache." )
);
}
}
}
}
// Check memory
#[cfg(target_os = "macos" )]
{
let vm_output = Command ::new ( "vm_stat" ). output ();
if let Ok ( output ) = vm_output {
let content = String ::from_utf8_lossy ( & output . stdout );
// Parse vm_stat output
let mut free_pages = 0 u64 ;
let mut active_pages = 0 u64 ;
let mut inactive_pages = 0 u64 ;
for line in content . lines () {
if line . contains ( "Pages free:" ) {
free_pages = line . split_whitespace (). nth ( 2 ). unwrap_or ( "0" ). trim_end_matches ( '.' ). parse (). unwrap_or ( 0 );
} else if line . contains ( "Pages active:" ) {
active_pages = line . split_whitespace (). nth ( 2 ). unwrap_or ( "0" ). trim_end_matches ( '.' ). parse (). unwrap_or ( 0 );
} else if line . contains ( "Pages inactive:" ) {
inactive_pages = line . split_whitespace (). nth ( 2 ). unwrap_or ( "0" ). trim_end_matches ( '.' ). parse (). unwrap_or ( 0 );
}
}
let page_size = 4096 u64 ;
let total_mb = (( free_pages + active_pages + inactive_pages ) * page_size ) / 1024 / 1024 ;
let free_mb = ( free_pages * page_size ) / 1024 / 1024 ;
result [ "memory" ] = json! ({
"total_mb" : total_mb ,
"free_mb" : free_mb ,
"unit" : "MB" ,
});
if free_mb < 2048 {
result [ "recommendations" ]. as_array_mut (). unwrap (). push (
json! ( "Low memory available. MT5 may crash during large optimizations." )
);
}
}
}
#[cfg(target_os = "linux" )]
{
let mem_output = Command ::new ( "free" ). args ([ "-m" ]). output ();
if let Ok ( output ) = mem_output {
let content = String ::from_utf8_lossy ( & output . stdout );
for line in content . lines () {
if line . starts_with ( "Mem:" ) {
let parts : Vec <& str > = line . split_whitespace (). collect ();
if parts . len () >= 4 {
result [ "memory" ] = json! ({
"total_mb" : parts [ 1 ]. parse ::< u64 > (). unwrap_or ( 0 ),
"used_mb" : parts [ 2 ]. parse ::< u64 > (). unwrap_or ( 0 ),
"free_mb" : parts [ 3 ]. parse ::< u64 > (). unwrap_or ( 0 ),
"unit" : "MB" ,
});
}
}
}
}
}
// Get CPU cores
let nproc_output = Command ::new ( "sysctl" )
. args ([ "-n" , "hw.ncpu" ])
. output ();
if let Ok ( output ) = nproc_output {
let cores = String ::from_utf8_lossy ( & output . stdout ). trim (). parse (). unwrap_or ( 0 );
result [ "cpu_cores" ] = json! ( cores );
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : false
}))
}
/// Validate MT5 configuration files
pub async fn handle_validate_mt5_config ( config : & Config , _args : & Value ) -> Result < Value > {
let mt5_dir = config . mt5_dir ()
. ok_or_else ( || anyhow ::anyhow! ( "MT5 directory not configured" )) ? ;
let mut result = json! ({
"terminal_ini" : null ,
"tester_ini" : null ,
"config_files_found" : Vec ::< String > ::new (),
"errors" : Vec ::< String > ::new (),
"warnings" : Vec ::< String > ::new (),
});
// Check terminal.ini
let terminal_ini = mt5_dir . join ( "terminal.ini" );
if terminal_ini . exists () {
result [ "config_files_found" ]. as_array_mut (). unwrap (). push ( json! ( "terminal.ini" ));
if let Ok ( content ) = fs ::read_to_string ( & terminal_ini ) {
// Check for common issues
if ! content . contains ( "[Common]" ) {
result [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "terminal.ini missing [Common] section" )
);
}
// Extract key settings
let mut settings = serde_json ::Map ::new ();
for line in content . lines () {
if line . starts_with ( "Login=" ) {
settings . insert ( "login" . to_string (), json! ( line . trim_start_matches ( "Login=" )));
} else if line . starts_with ( "Server=" ) {
settings . insert ( "server" . to_string (), json! ( line . trim_start_matches ( "Server=" )));
} else if line . starts_with ( "Expert=" ) {
settings . insert ( "expert" . to_string (), json! ( line . trim_start_matches ( "Expert=" )));
}
}
result [ "terminal_ini" ] = json! ( settings );
} else {
result [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "Could not read terminal.ini" )
);
}
} else {
result [ "warnings" ]. as_array_mut (). unwrap (). push (
json! ( "terminal.ini not found" )
);
}
// Check for tester config
let tester_dir = mt5_dir . join ( "Tester" );
if tester_dir . exists () {
if let Ok ( entries ) = fs ::read_dir ( & tester_dir ) {
let ini_files : Vec < String > = entries
. flatten ()
. filter_map ( | e | {
let p = e . path ();
if p . extension () ? . to_str () ? == "ini" {
Some ( p . file_name () ? . to_string_lossy (). to_string ())
} else {
None
}
})
. collect ();
if ! ini_files . is_empty () {
result [ "tester_ini" ] = json! ( ini_files );
}
}
}
// Check for common problems
let experts_dir = mt5_dir . join ( "MQL5" ). join ( "Experts" );
if ! experts_dir . exists () {
result [ "errors" ]. as_array_mut (). unwrap (). push (
json! ( "MQL5/Experts directory not found - MT5 installation may be incomplete" )
);
}
let has_errors = ! result [ "errors" ]. as_array (). unwrap (). is_empty ();
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : has_errors
}))
}
/// Get Wine prefix detailed information
pub async fn handle_get_wine_prefix_info ( config : & Config , _args : & Value ) -> Result < Value > {
let mt5_dir = config . mt5_dir ()
. ok_or_else ( || anyhow ::anyhow! ( "MT5 directory not configured" )) ? ;
let wine_prefix = mt5_dir
. parent ()
. and_then ( | p | p . parent ())
. and_then ( | p | p . parent ());
let mut result = json! ({
"prefix_path" : null ,
"exists" : false ,
"windows_version" : null ,
"dll_overrides" : Vec ::< String > ::new (),
"installed_programs" : Vec ::< String > ::new (),
"registry_files" : Vec ::< String > ::new (),
"drive_c_size_mb" : 0 ,
});
if let Some ( prefix ) = wine_prefix {
result [ "prefix_path" ] = json! ( prefix . to_string_lossy (). to_string ());
result [ "exists" ] = json! ( prefix . exists ());
if prefix . exists () {
// Check Windows version
let system_reg = prefix . join ( "system.reg" );
if system_reg . exists () {
if let Ok ( content ) = fs ::read_to_string ( & system_reg ) {
for line in content . lines (). take ( 50 ) {
if line . contains ( " \" ProductName \" " ) {
let parts : Vec <& str > = line . split ( '"' ). collect ();
if parts . len () >= 4 {
result [ "windows_version" ] = json! ( parts [ 3 ]);
}
}
}
}
}
// Calculate drive_c size
let drive_c = prefix . join ( "drive_c" );
if drive_c . exists () {
let mut size = 0 u64 ;
fn calc_size ( dir : & Path , total : & mut u64 ) {
if let Ok ( entries ) = fs ::read_dir ( dir ) {
for e in entries . flatten () {
let p = e . path ();
if p . is_file () {
if let Ok ( m ) = e . metadata () {
* total += m . len ();
}
} else if p . is_dir () {
calc_size ( & p , total );
}
}
}
}
calc_size ( & drive_c , & mut size );
result [ "drive_c_size_mb" ] = json! (( size / 1024 / 1024 ) as i64 );
}
// Check for installed programs
let prog_files = prefix . join ( "drive_c" ). join ( "Program Files" );
if prog_files . exists () {
if let Ok ( entries ) = fs ::read_dir ( & prog_files ) {
let programs : Vec < String > = entries
. flatten ()
. filter_map ( | e | {
let p = e . path ();
if p . is_dir () {
Some ( p . file_name () ? . to_string_lossy (). to_string ())
} else {
None
}
})
. collect ();
result [ "installed_programs" ] = json! ( programs );
}
}
// List registry files
let reg_files : Vec < String > = vec! [
"system.reg" , "user.reg" , "userdef.reg"
]
. into_iter ()
. filter ( | f | prefix . join ( f ). exists ())
. map ( | f | f . to_string ())
. collect ();
result [ "registry_files" ] = json! ( reg_files );
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : false
}))
}
/// Get backtest crash/failure information
pub async fn handle_get_backtest_crash_info ( config : & Config , args : & Value ) -> Result < Value > {
let report_dir = args . get ( "report_dir" ). and_then ( | v | v . as_str ());
let check_recent = args . get ( "check_recent" ). and_then ( | v | v . as_bool ()). unwrap_or ( true );
let hours_back = args . get ( "hours_back" ). and_then ( | v | v . as_u64 ()). unwrap_or ( 6 );
let mut result = json! ({
"crashes_found" : Vec ::< serde_json ::Value > ::new (),
"recent_failures" : 0 ,
"common_patterns" : Vec ::< String > ::new (),
});
// Check specific report directory if provided
if let Some ( dir ) = report_dir {
let path = Path ::new ( dir );
if path . exists () {
// Check for incomplete markers
let incomplete_marker = path . join ( ".incomplete" );
let error_log = path . join ( "error.log" );
if incomplete_marker . exists () {
result [ "crashes_found" ]. as_array_mut (). unwrap (). push ( json! ({
"report_dir" : dir ,
"type" : "incomplete" ,
"reason" : "Backtest was interrupted or timed out" ,
}));
}
if error_log . exists () {
if let Ok ( content ) = fs ::read_to_string ( & error_log ) {
result [ "crashes_found" ]. as_array_mut (). unwrap (). push ( json! ({
"report_dir" : dir ,
"type" : "error_log" ,
"content" : content . lines (). take ( 20 ). collect ::< Vec < _ >> (). join ( " \n " ),
}));
}
}
// Check if deals.csv is missing or empty
let deals_csv = path . join ( "deals.csv" );
if ! deals_csv . exists () {
result [ "crashes_found" ]. as_array_mut (). unwrap (). push ( json! ({
"report_dir" : dir ,
"type" : "missing_deals" ,
"reason" : "deals.csv not found - backtest likely failed" ,
}));
} else if let Ok ( meta ) = deals_csv . metadata () {
if meta . len () < 100 {
result [ "crashes_found" ]. as_array_mut (). unwrap (). push ( json! ({
"report_dir" : dir ,
"type" : "empty_deals" ,
"reason" : "deals.csv is nearly empty - no trades were made" ,
}));
}
}
}
}
// Check recent reports if requested
if check_recent {
let reports_dir_str = config . get ( "reports_dir" );
let reports_dir = Path ::new ( & reports_dir_str );
if reports_dir . exists () {
let cutoff = std ::time ::SystemTime ::now () - std ::time ::Duration ::from_secs ( hours_back * 3600 );
if let Ok ( entries ) = fs ::read_dir ( & reports_dir ) {
let mut failures = 0 ;
for entry in entries . flatten () {
let path = entry . path ();
if path . is_dir () {
if let Ok ( meta ) = entry . metadata () {
if let Ok ( modified ) = meta . modified () {
if modified >= cutoff {
// Check for failure indicators
let has_deals = path . join ( "deals.csv" ). exists ();
let has_incomplete = path . join ( ".incomplete" ). exists ();
if ! has_deals || has_incomplete {
failures += 1 ;
}
}
}
}
}
}
result [ "recent_failures" ] = json! ( failures );
}
}
}
// Analyze common patterns
let crashes = result [ "crashes_found" ]. as_array (). unwrap ();
if ! crashes . is_empty () {
let types : Vec < String > = crashes . iter ()
. filter_map ( | c | c . get ( "type" ). and_then ( | v | v . as_str ()). map ( | s | s . to_string ()))
. collect ();
if types . contains ( & "missing_deals" . to_string ()) {
result [ "common_patterns" ]. as_array_mut (). unwrap (). push (
json! ( "Missing deals.csv suggests MT5 crashed during backtest" )
);
}
if types . contains ( & "incomplete" . to_string ()) {
result [ "common_patterns" ]. as_array_mut (). unwrap (). push (
json! ( "Incomplete markers indicate interruptions - check system resources" )
);
}
}
Ok ( json! ({
"content" : [{ "type" : "text" , "text" : result . to_string () }],
"isError" : false
}))
}