277 lines
12 KiB
Plaintext
277 lines
12 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| Core/Logger.mqh |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef __LOGGER_MQH__
|
|
#define __LOGGER_MQH__
|
|
|
|
#include "Config.mqh"
|
|
#include "State.mqh"
|
|
|
|
extern EAState g_state;
|
|
|
|
class CLogger
|
|
{
|
|
private:
|
|
string m_logPath;
|
|
string m_label;
|
|
ulong m_magic;
|
|
int m_fileTrade;
|
|
int m_fileSignal;
|
|
int m_fileError;
|
|
bool m_initialized;
|
|
string m_panelName;
|
|
string m_objects[];
|
|
int m_objCount;
|
|
|
|
string TimeStampMicro() const
|
|
{
|
|
datetime t = TimeCurrent();
|
|
long msec = GetTickCount() % 1000;
|
|
return TimeToString(t, TIME_DATE|TIME_SECONDS) + "." + IntegerToString(msec, 3, '0');
|
|
}
|
|
|
|
bool EnsureDirectory(string path)
|
|
{
|
|
string dirs[];
|
|
int count = StringSplit(path, '\\', dirs);
|
|
string current = "";
|
|
for(int i = 0; i < count; i++)
|
|
{
|
|
if(i > 0) current += "\\";
|
|
current += dirs[i];
|
|
if(current == "") continue;
|
|
if(!FolderCreate(current, 0))
|
|
{
|
|
int err = GetLastError();
|
|
if(err != 183 && err != 0) return false; // 183 = already exists
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int OpenLogFile(string filename, string header)
|
|
{
|
|
string filepath = m_logPath + filename;
|
|
bool exists = FileIsExist(filepath);
|
|
int handle = FileOpen(filepath, FILE_WRITE|FILE_READ|FILE_CSV|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_ANSI, ',');
|
|
if(handle == INVALID_HANDLE) return INVALID_HANDLE;
|
|
FileSeek(handle, 0, SEEK_END);
|
|
if(!exists || FileTell(handle) == 0)
|
|
{
|
|
FileWrite(handle, header);
|
|
FileFlush(handle);
|
|
}
|
|
return handle;
|
|
}
|
|
|
|
void WriteCSV(int handle, string data)
|
|
{
|
|
if(handle == INVALID_HANDLE) return;
|
|
FileSeek(handle, 0, SEEK_END);
|
|
FileWriteString(handle, data + "\r\n");
|
|
FileFlush(handle);
|
|
}
|
|
|
|
void CreatePanel()
|
|
{
|
|
m_panelName = "MTF_Dashboard_" + IntegerToString((int)m_magic);
|
|
ObjectCreate(0, m_panelName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_XDISTANCE, 10);
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_YDISTANCE, 30);
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_XSIZE, 320);
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_YSIZE, 280);
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_BGCOLOR, C'20,20,30');
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
|
|
ObjectSetInteger(0, m_panelName, OBJPROP_COLOR, C'60,60,80');
|
|
|
|
CreateLabel("Title", 15, 35, "Universal MTF EA v2.0", 12, clrWhite, true);
|
|
CreateLabel("Section1", 15, 55, "=== MARKET STATE ===", 10, C'150,150,170');
|
|
CreateLabel("BiasLabel", 15, 72, "HTF Bias:", 9, clrSilver);
|
|
CreateLabel("BiasValue", 120, 72, "NEUTRAL", 9, clrYellow);
|
|
CreateLabel("RegimeLabel", 15, 88, "Regime:", 9, clrSilver);
|
|
CreateLabel("RegimeValue", 120, 88, "RANGE", 9, clrYellow);
|
|
CreateLabel("MLLabel", 15, 104, "ML Confidence:", 9, clrSilver);
|
|
CreateLabel("MLValue", 120, 104, "0.00", 9, clrYellow);
|
|
CreateLabel("Section2", 15, 122, "=== PERFORMANCE ===", 10, C'150,150,170');
|
|
CreateLabel("DailyLabel", 15, 139, "Daily PnL:", 9, clrSilver);
|
|
CreateLabel("DailyValue", 120, 139, "0.00", 9, clrWhite);
|
|
CreateLabel("WeeklyLabel", 15, 155, "Weekly PnL:", 9, clrSilver);
|
|
CreateLabel("WeeklyValue", 120, 155, "0.00", 9, clrWhite);
|
|
CreateLabel("TradesLabel", 15, 171, "Trades Today:", 9, clrSilver);
|
|
CreateLabel("TradesValue", 120, 171, "0", 9, clrWhite);
|
|
CreateLabel("Section3", 15, 189, "=== RISK STATUS ===", 10, C'150,150,170');
|
|
CreateLabel("OpenPosLabel", 15, 206, "Open Positions:", 9, clrSilver);
|
|
CreateLabel("OpenPosValue", 120, 206, "0", 9, clrWhite);
|
|
CreateLabel("RiskLabel", 15, 222, "Total Risk:", 9, clrSilver);
|
|
CreateLabel("RiskValue", 120, 222, "0.00%", 9, clrWhite);
|
|
CreateLabel("StatusLabel", 15, 238, "Status:", 9, clrSilver);
|
|
CreateLabel("StatusValue", 120, 238, "ACTIVE", 9, clrLime);
|
|
CreateLabel("Section4", 15, 256, "=== NEWS ===", 10, C'150,150,170');
|
|
CreateLabel("NewsLabel", 15, 273, "Next Event:", 9, clrSilver);
|
|
CreateLabel("NewsValue", 120, 273, "None", 9, clrWhite);
|
|
m_initialized = true;
|
|
}
|
|
|
|
void CreateLabel(string name, int x, int y, string text, int fontSize, color clr, bool bold = false)
|
|
{
|
|
string fullName = m_panelName + "_" + name;
|
|
ObjectCreate(0, fullName, OBJ_LABEL, 0, 0, 0);
|
|
ObjectSetInteger(0, fullName, OBJPROP_XDISTANCE, x);
|
|
ObjectSetInteger(0, fullName, OBJPROP_YDISTANCE, y);
|
|
ObjectSetString(0, fullName, OBJPROP_FONT, bold ? "Arial Bold" : "Arial");
|
|
ObjectSetInteger(0, fullName, OBJPROP_FONTSIZE, fontSize);
|
|
ObjectSetInteger(0, fullName, OBJPROP_COLOR, clr);
|
|
ObjectSetString(0, fullName, OBJPROP_TEXT, text);
|
|
ObjectSetInteger(0, fullName, OBJPROP_SELECTABLE, false);
|
|
int idx = ArraySize(m_objects);
|
|
ArrayResize(m_objects, idx + 1);
|
|
m_objects[idx] = fullName;
|
|
}
|
|
|
|
void UpdateLabel(string name, string text, color clr)
|
|
{
|
|
string fullName = m_panelName + "_" + name;
|
|
if(ObjectFind(0, fullName) >= 0)
|
|
{
|
|
ObjectSetString(0, fullName, OBJPROP_TEXT, text);
|
|
ObjectSetInteger(0, fullName, OBJPROP_COLOR, clr);
|
|
}
|
|
}
|
|
|
|
public:
|
|
CLogger() : m_fileTrade(INVALID_HANDLE), m_fileSignal(INVALID_HANDLE),
|
|
m_fileError(INVALID_HANDLE), m_initialized(false), m_objCount(0) {}
|
|
|
|
bool Init(string basePath, string label, ulong magic)
|
|
{
|
|
m_label = label; m_magic = magic; m_logPath = basePath;
|
|
if(StringLen(m_logPath) > 0 && StringSubstr(m_logPath, StringLen(m_logPath)-1) != "\\")
|
|
m_logPath += "\\";
|
|
m_logPath += label + "_" + IntegerToString((int)magic) + "\\";
|
|
if(!EnsureDirectory(m_logPath)) m_logPath = "";
|
|
|
|
string tradeHeader = "Timestamp,Symbol,Direction,EntryPrice,SL,TP1,TP2,Lot,RiskPercent,ATR_Value,Regime,HTF_Bias,Pattern,ExitPrice,ExitReason,PnL_USD,PnL_Percent,Duration_Minutes";
|
|
m_fileTrade = OpenLogFile("TradeJournal.csv", tradeHeader);
|
|
string signalHeader = "Timestamp,Symbol,HTF_Bias,MTF_Regime,LTF_Pattern,IsValid,RejectionReason";
|
|
m_fileSignal = OpenLogFile("SignalLog.csv", signalHeader);
|
|
string errorHeader = "Timestamp,Function,ErrorCode,ErrorMessage,RetryCount,Resolution";
|
|
m_fileError = OpenLogFile("ErrorLog.csv", errorHeader);
|
|
|
|
m_initialized = (m_fileTrade != INVALID_HANDLE && m_fileSignal != INVALID_HANDLE && m_fileError != INVALID_HANDLE);
|
|
if(m_initialized)
|
|
{
|
|
Print("[Logger] Audit trail active. Path: ", m_logPath);
|
|
CreatePanel();
|
|
}
|
|
return m_initialized;
|
|
}
|
|
|
|
void Shutdown()
|
|
{
|
|
if(m_fileTrade != INVALID_HANDLE) { FileClose(m_fileTrade); m_fileTrade = INVALID_HANDLE; }
|
|
if(m_fileSignal != INVALID_HANDLE) { FileClose(m_fileSignal); m_fileSignal = INVALID_HANDLE; }
|
|
if(m_fileError != INVALID_HANDLE) { FileClose(m_fileError); m_fileError = INVALID_HANDLE; }
|
|
for(int i = 0; i < ArraySize(m_objects); i++) ObjectDelete(0, m_objects[i]);
|
|
ObjectDelete(0, m_panelName);
|
|
Print("[Logger] Log files closed and dashboard cleared.");
|
|
}
|
|
|
|
void LogEvent(string category, string message)
|
|
{
|
|
if(InpDebugMode) Print("[", category, "] ", message);
|
|
}
|
|
|
|
void LogSignal(const SignalData &signal, const EAState &state)
|
|
{
|
|
if(m_fileSignal == INVALID_HANDLE) return;
|
|
string line = StringFormat("%s,%s,%s,%s,%s,%s,%s",
|
|
TimeStampMicro(), _Symbol, EnumToString(state.currentBias),
|
|
EnumToString(state.currentRegime), signal.patternName,
|
|
signal.isValid ? "YES" : "NO", signal.rejectionReason);
|
|
WriteCSV(m_fileSignal, line);
|
|
}
|
|
|
|
void LogError(string function, int code, string message, int retryCount)
|
|
{
|
|
if(m_fileError == INVALID_HANDLE) return;
|
|
string line = StringFormat("%s,%s,%d,%s,%d,%s",
|
|
TimeStampMicro(), function, code, message, retryCount, "PENDING");
|
|
WriteCSV(m_fileError, line);
|
|
}
|
|
|
|
void LogTradeOpen(const SignalData &signal, const TradeParams ¶ms, ulong ticket)
|
|
{
|
|
if(m_fileTrade == INVALID_HANDLE) return;
|
|
string dir = signal.isBuy ? "BUY" : "SELL";
|
|
string line = StringFormat("%s,%s,%s,%.5f,%.5f,%.5f,%.5f,%.2f,%.2f,%.5f,%s,%s,%s,%s,%.2f,%.2f,%d",
|
|
TimeStampMicro(), _Symbol, dir, signal.entryPrice, signal.slPrice,
|
|
signal.tp1Price, signal.tp2Price, params.lotSize, params.riskPercent,
|
|
signal.atrValue, EnumToString(g_state.currentRegime),
|
|
EnumToString(g_state.currentBias), signal.patternName, "", "", 0, 0, 0);
|
|
WriteCSV(m_fileTrade, line);
|
|
}
|
|
|
|
void LogTradeClose(const EAState &state)
|
|
{
|
|
if(m_fileTrade == INVALID_HANDLE) return;
|
|
string line = StringFormat("%s,%s,,%s,,,,,,,,,%.2f,%s,%.2f,%.0f",
|
|
TimeStampMicro(), _Symbol, EnumToString(state.lastExitReason),
|
|
state.lastTradePnL, EnumToString(state.lastExitReason),
|
|
(state.lastTradePnL / AccountInfoDouble(ACCOUNT_EQUITY)) * 100.0,
|
|
(TimeCurrent() - state.lastTradeClose) / 60.0);
|
|
WriteCSV(m_fileTrade, line);
|
|
}
|
|
|
|
void UpdateDashboard(const EAState &state)
|
|
{
|
|
color biasClr = clrYellow;
|
|
string biasText = EnumToString(state.currentBias);
|
|
if(state.currentBias == BIAS_BULL) biasClr = clrLime;
|
|
else if(state.currentBias == BIAS_BEAR) biasClr = clrRed;
|
|
UpdateLabel("BiasValue", biasText, biasClr);
|
|
|
|
color regimeClr = clrYellow;
|
|
string regimeText = EnumToString(state.currentRegime);
|
|
if(state.currentRegime == REGIME_TREND) regimeClr = clrLime;
|
|
else if(state.currentRegime == REGIME_CHOP) regimeClr = clrRed;
|
|
UpdateLabel("RegimeValue", regimeText, regimeClr);
|
|
|
|
color dailyClr = state.dailyPnL >= 0 ? clrLime : clrRed;
|
|
UpdateLabel("DailyValue", StringFormat("%.2f", state.dailyPnL), dailyClr);
|
|
|
|
color weeklyClr = state.weeklyPnL >= 0 ? clrLime : clrRed;
|
|
UpdateLabel("WeeklyValue", StringFormat("%.2f", state.weeklyPnL), weeklyClr);
|
|
|
|
UpdateLabel("TradesValue", IntegerToString(state.totalTradesToday), clrWhite);
|
|
UpdateLabel("OpenPosValue", IntegerToString(state.openPositions), state.openPositions > 0 ? clrLime : clrWhite);
|
|
|
|
color riskClr = state.totalOpenRisk > InpMaxTotalRisk * 0.8 ? clrRed :
|
|
state.totalOpenRisk > InpMaxTotalRisk * 0.5 ? clrYellow : clrWhite;
|
|
UpdateLabel("RiskValue", StringFormat("%.2f%%", state.totalOpenRisk), riskClr);
|
|
|
|
string status = "ACTIVE";
|
|
color statusClr = clrLime;
|
|
if(state.dailyLimitHit) { status = "DAILY LIMIT"; statusClr = clrRed; }
|
|
else if(state.weeklyLimitHit) { status = "WEEKLY LIMIT"; statusClr = clrRed; }
|
|
else if(state.consecLossHalted) { status = "CONSEC LOSS"; statusClr = clrRed; }
|
|
else if(state.circuitBreakerUntil > TimeCurrent()) { status = "HALTED"; statusClr = clrRed; }
|
|
UpdateLabel("StatusValue", status, statusClr);
|
|
|
|
string dash = StringFormat(
|
|
"\n=== Universal_MTF_EA v2.0 | %s ===\n"
|
|
"Bias: %s | Regime: %s | Volume: %s\n"
|
|
"Daily PnL: %.2f | Weekly PnL: %.2f\n"
|
|
"Open Pos: %d | Total Risk: %.2f%%\n"
|
|
"Last Trade: %.2f (%s)\n"
|
|
"Status: %s\n"
|
|
"====================",
|
|
_Symbol, EnumToString(state.currentBias), EnumToString(state.currentRegime),
|
|
state.volumeConfirmed ? "OK" : "LOW", state.dailyPnL, state.weeklyPnL,
|
|
state.openPositions, state.totalOpenRisk, state.lastTradePnL,
|
|
EnumToString(state.lastExitReason), status);
|
|
Comment(dash);
|
|
}
|
|
};
|
|
|
|
#endif // __LOGGER_MQH__
|