//+------------------------------------------------------------------+ //| Utilities.mqh | //| Utility Functions Module 2.2 | //| Fixed: Backtest compatibility | //+------------------------------------------------------------------+ #ifndef UTILITIES_MQH #define UTILITIES_MQH #include "DataTypes.mqh" #include //+------------------------------------------------------------------+ //| Utilities Class | //+------------------------------------------------------------------+ class CUtilities { private: //--- Configuration ENUM_LOG_LEVEL m_log_level; string m_log_filename; string m_report_filename; bool m_is_testing; // Add flag for testing mode //--- File handles int m_log_handle; //--- News calendar (simplified) datetime m_news_times[]; string m_news_impacts[]; int m_news_count; //--- Helper methods string GetTimeString(datetime time); string GetLogPrefix(ENUM_LOG_LEVEL level); void WriteToFile(string filename, string text); public: CUtilities(); ~CUtilities(); //--- Initialization bool Initialize(ENUM_LOG_LEVEL log_level); //--- Logging void Log(ENUM_LOG_LEVEL level, string message); void LogTrade(const ManagedTrade &trade, string action); void LogError(string function, int error_code); //--- Reporting void GenerateReport(const PerformanceMetrics &performance, const ManagedTrade &trades[]); void SaveTradeHistory(const ManagedTrade &trade); void ExportPerformanceCSV(const PerformanceMetrics &performance); //--- Time functions bool IsNewsTime(int buffer_minutes = 30); bool IsTradingHours(); bool IsMarketClosed(); string GetDayOfWeek(); //--- Formatting string FormatCurrency(double value, string currency = "USD"); string FormatPercent(double value, int digits = 2); string FormatPips(double points); string FormatDuration(int seconds); //--- Calculations double CalculatePips(double price_difference); double CalculatePositionCommission(double volume); double CalculateSwap(const ManagedTrade &trade); double CalculatePositionCommission(ulong position_ticket); //--- System functions string GetUninitReasonText(int reason); bool CheckTerminalConnection(); bool CheckAutoTrading(); void SendNotification(string message, bool email = true, bool push = true, bool alert = true); //--- File operations bool SaveSettings(string filename); bool LoadSettings(string filename); void BackupData(); //--- Market helpers double NormalizeLots(double lots); double NormalizePrice(double price); bool IsNewBar(ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT); double GetSpreadInPips(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CUtilities::CUtilities() { m_log_level = LOG_INFO; m_log_filename = "RiskManager_Log.txt"; m_report_filename = "RiskManager_Report.txt"; m_log_handle = INVALID_HANDLE; m_news_count = 0; m_is_testing = false; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CUtilities::~CUtilities() { if(m_log_handle != INVALID_HANDLE) FileClose(m_log_handle); } //+------------------------------------------------------------------+ //| Initialize utilities | //+------------------------------------------------------------------+ bool CUtilities::Initialize(ENUM_LOG_LEVEL log_level) { m_log_level = log_level; //--- Check if we're in testing mode m_is_testing = MQLInfoInteger(MQL_TESTER); //--- Only create log file if NOT in testing mode if(!m_is_testing) { m_log_handle = FileOpen(m_log_filename, FILE_WRITE|FILE_TXT|FILE_ANSI); if(m_log_handle == INVALID_HANDLE) { Print("Utilities: Failed to create log file"); // Don't fail initialization, just disable file logging m_log_handle = INVALID_HANDLE; } else { //--- Write header string header = "=== Risk Management Overlay v6.6 Log ===\n"; header += "Started: " + TimeToString(TimeCurrent()) + "\n"; header += "Symbol: " + _Symbol + "\n"; header += "Period: " + EnumToString((ENUM_TIMEFRAMES)_Period) + "\n"; header += "=========================================\n"; FileWrite(m_log_handle, header); FileFlush(m_log_handle); } } //--- Load news calendar (would normally load from file/API) // Simplified example - add some dummy news times ArrayResize(m_news_times, 5); ArrayResize(m_news_impacts, 5); m_news_count = 0; Print("Utilities initialized: Testing=", m_is_testing, ", LogLevel=", EnumToString(log_level)); return true; } //+------------------------------------------------------------------+ //| Log message | //+------------------------------------------------------------------+ void CUtilities::Log(ENUM_LOG_LEVEL level, string message) { if(level > m_log_level) return; string log_entry = GetTimeString(TimeCurrent()) + " " + GetLogPrefix(level) + " " + message; //--- Always print to terminal in testing mode or for important messages if(m_is_testing || level <= LOG_WARNING) Print(log_entry); //--- Write to file only if not testing and file is open if(!m_is_testing && m_log_handle != INVALID_HANDLE) { FileWrite(m_log_handle, log_entry); FileFlush(m_log_handle); } } //+------------------------------------------------------------------+ //| Generate performance report | //+------------------------------------------------------------------+ void CUtilities::GenerateReport(const PerformanceMetrics &performance, const ManagedTrade &trades[]) { string report = "\n=== PERFORMANCE REPORT ===\n"; report += "Generated: " + TimeToString(TimeCurrent()) + "\n\n"; //--- Account Summary report += "ACCOUNT SUMMARY\n"; report += "---------------\n"; report += "Starting Balance: " + FormatCurrency(AccountInfoDouble(ACCOUNT_BALANCE) - performance.net_profit) + "\n"; report += "Current Balance: " + FormatCurrency(AccountInfoDouble(ACCOUNT_BALANCE)) + "\n"; report += "Net Profit: " + FormatCurrency(performance.net_profit) + " (" + FormatPercent(performance.net_profit / (AccountInfoDouble(ACCOUNT_BALANCE) - performance.net_profit) * 100) + ")\n"; report += "Current Equity: " + FormatCurrency(AccountInfoDouble(ACCOUNT_EQUITY)) + "\n\n"; //--- Trading Statistics report += "TRADING STATISTICS\n"; report += "-----------------\n"; report += "Total Trades: " + IntegerToString(performance.total_trades) + "\n"; report += "Winning Trades: " + IntegerToString(performance.winning_trades) + " (" + FormatPercent(performance.win_rate) + ")\n"; report += "Losing Trades: " + IntegerToString(performance.losing_trades) + "\n"; report += "Gross Profit: " + FormatCurrency(performance.gross_profit) + "\n"; report += "Gross Loss: " + FormatCurrency(performance.gross_loss) + "\n"; report += "Profit Factor: " + DoubleToString(performance.profit_factor, 2) + "\n"; report += "Expectancy: " + FormatCurrency(performance.expectancy) + "\n\n"; //--- Risk Metrics report += "RISK METRICS\n"; report += "------------\n"; report += "Max Drawdown: " + FormatCurrency(performance.max_drawdown_amount) + " (" + FormatPercent(performance.max_drawdown_percent) + ")\n"; report += "Sharpe Ratio: " + DoubleToString(performance.sharpe_ratio, 2) + "\n"; report += "Sortino Ratio: " + DoubleToString(performance.sortino_ratio, 2) + "\n"; report += "Calmar Ratio: " + DoubleToString(performance.calmar_ratio, 2) + "\n"; report += "Recovery Factor: " + DoubleToString(performance.recovery_factor, 2) + "\n\n"; //--- Consecutive Stats report += "CONSECUTIVE STATISTICS\n"; report += "--------------------\n"; report += "Current Consecutive Wins: " + IntegerToString(performance.consecutive_wins) + "\n"; report += "Current Consecutive Losses: " + IntegerToString(performance.consecutive_losses) + "\n"; report += "Max Consecutive Wins: " + IntegerToString(performance.max_consecutive_wins) + "\n"; report += "Max Consecutive Losses: " + IntegerToString(performance.max_consecutive_losses) + "\n\n"; //--- Active Trades if(ArraySize(trades) > 0) { report += "ACTIVE TRADES\n"; report += "-------------\n"; for(int i = 0; i < ArraySize(trades); i++) { report += StringFormat("#%d %s %.2f @ %.5f | P/L: %s (%.2fR) | %s\n", trades[i].ticket, (trades[i].type == POSITION_TYPE_BUY) ? "BUY" : "SELL", trades[i].volume, trades[i].open_price, FormatCurrency(trades[i].profit), trades[i].r_multiple, trades[i].entry_reason); } } report += "\n========================\n"; //--- In testing mode, just print the report if(m_is_testing) { Print(report); } else { //--- Save to file only if not testing WriteToFile(m_report_filename, report); } //--- Also log summary Log(LOG_INFO, "Performance report generated"); } //+------------------------------------------------------------------+ //| Write to file | //+------------------------------------------------------------------+ void CUtilities::WriteToFile(string filename, string text) { //--- Skip file operations in testing mode if(m_is_testing) return; int handle = FileOpen(filename, FILE_WRITE|FILE_TXT|FILE_ANSI); if(handle != INVALID_HANDLE) { FileWrite(handle, text); FileClose(handle); } } //+------------------------------------------------------------------+ //| Save trade history | //+------------------------------------------------------------------+ void CUtilities::SaveTradeHistory(const ManagedTrade &trade) { //--- Skip file operations in testing mode if(m_is_testing) { //--- Just log the trade details instead string trade_info = StringFormat("Trade Closed: #%d %s %.2f lots, P/L: %.2f, R: %.2f", trade.ticket, (trade.type == POSITION_TYPE_BUY) ? "BUY" : "SELL", trade.volume, trade.profit, trade.r_multiple); Log(LOG_INFO, trade_info); return; } string filename = "TradeHistory_" + _Symbol + ".csv"; //--- Check if file exists to write header bool file_exists = FileIsExist(filename); int handle = FileOpen(filename, FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_SHARE_READ, ','); if(handle == INVALID_HANDLE) return; //--- Write header if new file if(!file_exists) { FileWrite(handle, "Ticket", "Symbol", "Type", "OpenTime", "CloseTime", "OpenPrice", "ClosePrice", "Volume", "Profit", "Commission", "Swap", "RMultiple", "MAE", "MFE", "EntryReason"); } //--- Move to end of file FileSeek(handle, 0, SEEK_END); //--- Write trade data FileWrite(handle, trade.ticket, trade.symbol, (trade.type == POSITION_TYPE_BUY) ? "BUY" : "SELL", trade.open_time, TimeCurrent(), trade.open_price, SymbolInfoDouble(_Symbol, SYMBOL_BID), trade.volume, trade.profit, trade.commission, trade.swap, trade.r_multiple, trade.mae, trade.mfe, trade.entry_reason); FileClose(handle); } //+------------------------------------------------------------------+ //| Export performance to CSV | //+------------------------------------------------------------------+ void CUtilities::ExportPerformanceCSV(const PerformanceMetrics &performance) { //--- Skip file operations in testing mode if(m_is_testing) return; string filename = "Performance_" + _Symbol + "_" + TimeToString(TimeCurrent(), TIME_DATE) + ".csv"; StringReplace(filename, ".", "_"); int handle = FileOpen(filename, FILE_WRITE|FILE_CSV|FILE_ANSI, ','); if(handle == INVALID_HANDLE) return; //--- Write header FileWrite(handle, "Metric", "Value"); //--- Write metrics FileWrite(handle, "Total Trades", performance.total_trades); FileWrite(handle, "Winning Trades", performance.winning_trades); FileWrite(handle, "Losing Trades", performance.losing_trades); FileWrite(handle, "Win Rate %", performance.win_rate); FileWrite(handle, "Gross Profit", performance.gross_profit); FileWrite(handle, "Gross Loss", performance.gross_loss); FileWrite(handle, "Net Profit", performance.net_profit); FileWrite(handle, "Profit Factor", performance.profit_factor); FileWrite(handle, "Expectancy", performance.expectancy); FileWrite(handle, "Average Win", performance.avg_win); FileWrite(handle, "Average Loss", performance.avg_loss); FileWrite(handle, "Max Drawdown %", performance.max_drawdown_percent); FileWrite(handle, "Max Drawdown Amount", performance.max_drawdown_amount); FileWrite(handle, "Sharpe Ratio", performance.sharpe_ratio); FileWrite(handle, "Sortino Ratio", performance.sortino_ratio); FileWrite(handle, "Calmar Ratio", performance.calmar_ratio); FileWrite(handle, "Max Consecutive Wins", performance.max_consecutive_wins); FileWrite(handle, "Max Consecutive Losses", performance.max_consecutive_losses); FileClose(handle); Log(LOG_INFO, "Performance exported to " + filename); } //+------------------------------------------------------------------+ //| Backup important data | //+------------------------------------------------------------------+ void CUtilities::BackupData() { //--- Skip file operations in testing mode if(m_is_testing) return; //--- Create backup filename with timestamp string timestamp = TimeToString(TimeCurrent(), TIME_DATE); StringReplace(timestamp, ".", "_"); string backup_dir = "Backups\\" + timestamp + "\\"; //--- Copy important files if(FileIsExist(m_log_filename)) FileCopy(m_log_filename, 0, backup_dir + m_log_filename, FILE_REWRITE); if(FileIsExist(m_report_filename)) FileCopy(m_report_filename, 0, backup_dir + m_report_filename, FILE_REWRITE); Log(LOG_INFO, "Data backed up to " + backup_dir); } // ... [Rest of the functions remain the same] //+------------------------------------------------------------------+ //| Log trade action | //+------------------------------------------------------------------+ void CUtilities::LogTrade(const ManagedTrade &trade, string action) { string message = StringFormat("Trade #%d %s: %s %.2f lots at %.5f, P/L: %s", trade.ticket, action, (trade.type == POSITION_TYPE_BUY) ? "BUY" : "SELL", trade.volume, trade.open_price, FormatCurrency(trade.profit)); Log(LOG_INFO, message); } //+------------------------------------------------------------------+ //| Log error | //+------------------------------------------------------------------+ void CUtilities::LogError(string function, int error_code) { string message = StringFormat("Error in %s: %d - %s", function, error_code, GetLastError()); Log(LOG_ERROR, message); } //+------------------------------------------------------------------+ //| Check if news time | //+------------------------------------------------------------------+ bool CUtilities::IsNewsTime(int buffer_minutes) { datetime current_time = TimeCurrent(); for(int i = 0; i < m_news_count; i++) { if(MathAbs((double)(current_time - m_news_times[i])) < buffer_minutes * 60) { if(m_news_impacts[i] == "HIGH") return true; } } return false; } //+------------------------------------------------------------------+ //| Check if within trading hours | //+------------------------------------------------------------------+ bool CUtilities::IsTradingHours() { MqlDateTime time; TimeCurrent(time); //--- Skip weekends if(time.day_of_week == 0 || time.day_of_week == 6) return false; //--- Skip Friday after 20:00 if(time.day_of_week == 5 && time.hour >= 20) return false; //--- Skip Sunday before 22:00 if(time.day_of_week == 0 && time.hour < 22) return false; return true; } //+------------------------------------------------------------------+ //| Check if market is closed | //+------------------------------------------------------------------+ bool CUtilities::IsMarketClosed() { //--- Check if we're getting price updates static datetime last_tick_time = 0; datetime current_tick_time = (datetime)SymbolInfoInteger(_Symbol, SYMBOL_TIME); if(current_tick_time == last_tick_time && TimeCurrent() - current_tick_time > 120) // No tick for 2 minutes { return true; } last_tick_time = current_tick_time; return false; } //+------------------------------------------------------------------+ //| Format currency value | //+------------------------------------------------------------------+ string CUtilities::FormatCurrency(double value, string currency) { string sign = value >= 0 ? "" : "-"; string formatted = sign + currency + " " + DoubleToString(MathAbs(value), 2); return formatted; } //+------------------------------------------------------------------+ //| Format percentage | //+------------------------------------------------------------------+ string CUtilities::FormatPercent(double value, int digits) { return DoubleToString(value, digits) + "%"; } //+------------------------------------------------------------------+ //| Format pips | //+------------------------------------------------------------------+ string CUtilities::FormatPips(double points) { double pips = CalculatePips(points); return DoubleToString(pips, 1) + " pips"; } //+------------------------------------------------------------------+ //| Calculate pips from points | //+------------------------------------------------------------------+ double CUtilities::CalculatePips(double price_difference) { double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); double pip_value = (digits == 3 || digits == 5) ? point * 10 : point; return price_difference / pip_value; } //+------------------------------------------------------------------+ //| Get spread in pips | //+------------------------------------------------------------------+ double CUtilities::GetSpreadInPips() { double spread_points = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * SymbolInfoDouble(_Symbol, SYMBOL_POINT); return CalculatePips(spread_points); } //+------------------------------------------------------------------+ //| Send notification | //+------------------------------------------------------------------+ void CUtilities::SendNotification(string message, bool email, bool push, bool alert) { if(alert) Alert(message); if(email && TerminalInfoInteger(TERMINAL_EMAIL_ENABLED)) SendMail("Risk Management Alert", message); if(push && TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) SendNotification(message); Log(LOG_INFO, "Notification sent: " + message); } //+------------------------------------------------------------------+ //| Get time string | //+------------------------------------------------------------------+ string CUtilities::GetTimeString(datetime time) { return TimeToString(time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); } //+------------------------------------------------------------------+ //| Get log prefix | //+------------------------------------------------------------------+ string CUtilities::GetLogPrefix(ENUM_LOG_LEVEL level) { switch(level) { case LOG_ERROR: return "[ERROR]"; case LOG_WARNING: return "[WARN ]"; case LOG_INFO: return "[INFO ]"; case LOG_DEBUG: return "[DEBUG]"; default: return "[?????]"; } } //+------------------------------------------------------------------+ //| Get uninit reason text | //+------------------------------------------------------------------+ string CUtilities::GetUninitReasonText(int reason) { switch(reason) { case REASON_PROGRAM: return "Program terminated"; case REASON_REMOVE: return "Program removed from chart"; case REASON_RECOMPILE: return "Program recompiled"; case REASON_CHARTCHANGE: return "Symbol or timeframe changed"; case REASON_CHARTCLOSE: return "Chart closed"; case REASON_PARAMETERS: return "Input parameters changed"; case REASON_ACCOUNT: return "Account changed"; case REASON_TEMPLATE: return "New template applied"; case REASON_INITFAILED: return "OnInit() failed"; case REASON_CLOSE: return "Terminal closed"; default: return "Unknown reason"; } } //+------------------------------------------------------------------+ //| Normalize lot size | //+------------------------------------------------------------------+ double CUtilities::NormalizeLots(double lots) { double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); //--- Normalize to lot step lots = MathRound(lots / lot_step) * lot_step; //--- Apply limits lots = MathMax(lots, min_lot); lots = MathMin(lots, max_lot); return NormalizeDouble(lots, 2); } //+------------------------------------------------------------------+ //| Normalize price | //+------------------------------------------------------------------+ double CUtilities::NormalizePrice(double price) { int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); return NormalizeDouble(price, digits); } //+------------------------------------------------------------------+ //| Check for new bar | //+------------------------------------------------------------------+ bool CUtilities::IsNewBar(ENUM_TIMEFRAMES timeframe) { static datetime last_bar_time = 0; datetime current_bar_time = iTime(_Symbol, timeframe, 0); if(current_bar_time != last_bar_time) { last_bar_time = current_bar_time; return true; } return false; } //+------------------------------------------------------------------+ //| Check terminal connection | //+------------------------------------------------------------------+ bool CUtilities::CheckTerminalConnection() { if(!TerminalInfoInteger(TERMINAL_CONNECTED)) { Log(LOG_ERROR, "Terminal not connected to broker"); return false; } if(IsMarketClosed()) { Log(LOG_WARNING, "Market appears to be closed"); return false; } return true; } //+------------------------------------------------------------------+ //| Check auto trading | //+------------------------------------------------------------------+ bool CUtilities::CheckAutoTrading() { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Log(LOG_ERROR, "Auto trading not allowed in terminal"); return false; } if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Log(LOG_ERROR, "Auto trading not allowed for this EA"); return false; } if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) { Log(LOG_ERROR, "Auto trading not allowed for this account"); return false; } return true; } //+------------------------------------------------------------------+ //| Calculate commission for a position from deal history | //+------------------------------------------------------------------+ double CUtilities::CalculatePositionCommission(ulong position_ticket) { double total_commission = 0; // Select position to get its ID if(!PositionSelectByTicket(position_ticket)) return 0; long position_id = PositionGetInteger(POSITION_IDENTIFIER); // Iterate through history deals if(HistorySelectByPosition(position_id)) { int deals = HistoryDealsTotal(); for(int i = 0; i < deals; i++) { ulong deal_ticket = HistoryDealGetTicket(i); if(deal_ticket > 0) { total_commission += HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); } } } return total_commission; } //+------------------------------------------------------------------+ //| Format duration | //+------------------------------------------------------------------+ string CUtilities::FormatDuration(int seconds) { int days = seconds / 86400; int hours = (seconds % 86400) / 3600; int minutes = (seconds % 3600) / 60; string result = ""; if(days > 0) result += IntegerToString(days) + "d "; if(hours > 0) result += IntegerToString(hours) + "h "; if(minutes > 0) result += IntegerToString(minutes) + "m"; return result; } //+------------------------------------------------------------------+ //| Get day of week string | //+------------------------------------------------------------------+ string CUtilities::GetDayOfWeek() { MqlDateTime time; TimeCurrent(time); string days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; return days[time.day_of_week]; } #endif // UTILITIES_MQH //+------------------------------------------------------------------+