#include "RuntimeStorage.mqh" //+------------------------------------------------------------------+ //| UnifiedTradeLogger.mqh | //| Purpose: Comprehensive JSON logging for complete trade lifecycle | //| Features: Execution + Close logging with gate journey details | //+------------------------------------------------------------------+ #ifndef __UNIFIEDTRADELOGGER_MQH__ #define __UNIFIEDTRADELOGGER_MQH__ #include "AdaptiveSignalOptimizer.mqh" #include // Complete trade record struct UnifiedTradeRecord { // Trade identification string trade_id; string signal_id; datetime execution_time; datetime close_time; // Strategy and market string strategy_name; string symbol; int timeframe; string market_regime; double volatility; // Execution details double entry_price; double exit_price; double sl; double tp; double volume; int order_type; // Gate journey GateJourneyStep gate_journey[24]; int journey_length; // Optimization details bool was_adjusted; int adjustment_attempts; int blocking_gate; // Which gate initially blocked (-1 if none) // Outcome bool is_closed; bool was_winner; double profit_pct; double profit_absolute; double hold_duration_minutes; // Learning metadata double confidence; string close_reason; // "profit_target", "stop_loss", "signal_reversal", etc. // Position reviews int reviews_performed; string review_actions[]; // "HOLD", "ADJUST_SL", etc. // Execution callers populate only the fields they have. In particular, // an untouched journey length must never be interpreted as an array bound. UnifiedTradeRecord() { execution_time = 0; close_time = 0; timeframe = 0; volatility = 0.0; entry_price = 0.0; exit_price = 0.0; sl = 0.0; tp = 0.0; volume = 0.0; order_type = 0; journey_length = 0; was_adjusted = false; adjustment_attempts = 0; blocking_gate = -1; is_closed = false; was_winner = false; profit_pct = 0.0; profit_absolute = 0.0; hold_duration_minutes = 0.0; confidence = 0.0; reviews_performed = 0; ZeroMemory(gate_journey); } }; //+------------------------------------------------------------------+ //| Unified Trade Logger - Complete lifecycle tracking | //+------------------------------------------------------------------+ class CUnifiedTradeLogger { private: string m_log_directory; string m_daily_log_file; datetime m_current_day; int m_trades_logged; int m_trades_closed; string EscapeJSON(string value) { string escaped = ""; for(int i=0; i 0) { if(ch == 34) quoted = true; else if(ch == 123) depth++; else if(ch == 125) { depth--; if(depth == 0) { int count = ArraySize(trades); if(ArrayResize(trades, count+1) != count+1) return false; trades[count] = StringSubstr(document, start, i-start+1); need_comma = true; } } continue; } if(IsJSONSpace(ch)) continue; if(ch == 93) { // A comma must be followed by another object, not the array end. if(!need_comma && ArraySize(trades) > 0) return false; string suffix = StringSubstr(document, i+1); StringTrimLeft(suffix); StringTrimRight(suffix); return suffix == "}"; } if(need_comma) { if(ch != 44) return false; need_comma = false; continue; } if(ch != 123) return false; start = i; depth = 1; } return false; } bool MatchesTradeId(const string trade, const string trade_id) { int key = StringFind(trade, "\"trade_id\""); if(key < 0) return false; int pos = key + 10; int length = StringLen(trade); while(pos < length && IsJSONSpace(StringGetCharacter(trade, pos))) pos++; if(pos >= length || StringGetCharacter(trade, pos++) != 58) return false; while(pos < length && IsJSONSpace(StringGetCharacter(trade, pos))) pos++; if(pos >= length || StringGetCharacter(trade, pos++) != 34) return false; int start = pos; bool escaped = false; for(; pos 0 ? log_date : TimeCurrent(), TIME_DATE)); document += " \"trades\": [\n"; for(int i=0; i 0) document += ",\n"; document += trades[i]; } document += "\n ]\n}\n"; string temporary = m_daily_log_file + ".tmp"; uchar bytes[]; int length = StringToCharArray(document, bytes, 0, WHOLE_ARRAY, CP_UTF8) - 1; if(length <= 0) return false; int handle = FileOpen(temporary, FILE_WRITE|FILE_BIN|DUALEA_FILE_COMMON); if(handle == INVALID_HANDLE) return false; uint written = FileWriteArray(handle, bytes, 0, length); FileFlush(handle); FileClose(handle); if(written != (uint)length || !FileMove(temporary, DUALEA_FILE_COMMON, m_daily_log_file, DUALEA_FILE_COMMON|FILE_REWRITE)) { FileDelete(temporary, DUALEA_FILE_COMMON); return false; } return true; } // Get current day's log file string GetDailyLogFile() { MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); string date_str = StringFormat("%04d%02d%02d", dt.year, dt.mon, dt.day); return m_log_directory + "\\" + "trades_" + date_str + ".json"; } // Check if day changed bool CheckDayChange() { MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); datetime today = StringToTime(StringFormat("%04d.%02d.%02d", dt.year, dt.mon, dt.day)); if(today != m_current_day) { m_current_day = today; m_daily_log_file = GetDailyLogFile(); return true; } return false; } public: CUnifiedTradeLogger(string directory="DualEA\\trade_logs") { m_log_directory = directory; FolderCreate("DualEA", DUALEA_FILE_COMMON); FolderCreate(m_log_directory, DUALEA_FILE_COMMON); m_current_day = 0; m_trades_logged = 0; m_trades_closed = 0; CheckDayChange(); PrintFormat("šŸ“ UnifiedTradeLogger initialized: %s", m_daily_log_file); } // Log trade at execution (preliminary record). Every successful write is // a complete JSON document; no fixed-offset tail overwrites are used. void LogTradeExecution(const UnifiedTradeRecord &record) { CheckDayChange(); string trades[]; if(!ReadTrades(trades)) { Print("[TradeLogger] Existing log is unreadable or malformed; preserving it: ", m_daily_log_file); return; } int count = ArraySize(trades); if(ArrayResize(trades, count+1) != count+1) return; trades[count] = FormatTradeAsJSON(record, false); if(!WriteTrades(trades)) { Print("[TradeLogger] Could not commit execution record: ", EscapeJSON(record.trade_id)); return; } m_trades_logged++; PrintFormat("[TradeLogger] Execution logged: %s (Total: %d)", record.trade_id, m_trades_logged); } // Locate the execution-day file so a close after midnight updates the // original trade instead of searching only today's file. void LogTradeClose(const UnifiedTradeRecord &record) { CheckDayChange(); string current_file = m_daily_log_file; if(record.execution_time > 0) { MqlDateTime dt; TimeToStruct(record.execution_time, dt); m_daily_log_file = m_log_directory + "\\trades_" + StringFormat("%04d%02d%02d", dt.year, dt.mon, dt.day) + ".json"; } string trades[]; bool committed = false; if(ReadTrades(trades)) { for(int i=0; i 0) json += ",\n"; json += " {\n"; json += StringFormat(" \"gate\": \"%s\",\n", EscapeJSON(step.gate_name)); json += StringFormat(" \"index\": %d,\n", step.gate_index); json += StringFormat(" \"passed\": %s,\n", step.passed ? "true" : "false"); json += StringFormat(" \"reason\": \"%s\",\n", EscapeJSON(step.reason)); json += StringFormat(" \"adjusted\": %s", step.was_adjusted ? "true" : "false"); if(step.was_adjusted) { json += ",\n \"adjustments\": {\n"; json += StringFormat(" \"price\": [%.5f, %.5f],\n", step.price_before, step.price_after); json += StringFormat(" \"sl\": [%.5f, %.5f],\n", step.sl_before, step.sl_after); json += StringFormat(" \"tp\": [%.5f, %.5f],\n", step.tp_before, step.tp_after); json += StringFormat(" \"volume\": [%.2f, %.2f]\n", step.volume_before, step.volume_after); json += " }\n"; } else { json += "\n"; } json += " }"; } json += "\n ],\n"; // Optimization info json += " \"optimization\": {\n"; json += StringFormat(" \"was_adjusted\": %s,\n", record.was_adjusted ? "true" : "false"); json += StringFormat(" \"attempts\": %d,\n", record.adjustment_attempts); json += StringFormat(" \"blocking_gate\": %d\n", record.blocking_gate); json += " }"; // Final outcome (if closed) if(is_final) { json += ",\n \"outcome\": {\n"; json += StringFormat(" \"close_time\": \"%s\",\n", TimeToString(record.close_time)); json += StringFormat(" \"exit_price\": %.5f,\n", record.exit_price); json += StringFormat(" \"profit_pct\": %.2f,\n", record.profit_pct); json += StringFormat(" \"profit_absolute\": %.2f,\n", record.profit_absolute); json += StringFormat(" \"duration_minutes\": %.1f,\n", record.hold_duration_minutes); json += StringFormat(" \"was_winner\": %s,\n", record.was_winner ? "true" : "false"); json += StringFormat(" \"close_reason\": \"%s\",\n", EscapeJSON(record.close_reason)); json += StringFormat(" \"reviews_performed\": %d\n", record.reviews_performed); json += " }"; } json += "\n }"; return json; } // Get statistics void GetStatistics(int &total_logged, int &total_closed) { total_logged = m_trades_logged; total_closed = m_trades_closed; } void PrintReport() { PrintFormat("\nšŸ“ === Unified Trade Logger Report ==="); PrintFormat("Trades Logged: %d", m_trades_logged); PrintFormat("Trades Closed: %d", m_trades_closed); PrintFormat("Current Log File: %s", m_daily_log_file); PrintFormat("=====================================\n"); } }; #endif