mql5/Experts/Advisors/Modules/Utilities.mqh

759 lines
29 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| Utilities.mqh |
//| Utility Functions Module 2.2 |
//| Fixed: Backtest compatibility |
//+------------------------------------------------------------------+
#ifndef UTILITIES_MQH
#define UTILITIES_MQH
#include "DataTypes.mqh"
#include <Files/FileTxt.mqh>
//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+