//+------------------------------------------------------------------+ //| CShadowLogger.mqh - Shadow Mode Logging System | //| Based on chat-8Stage Gate System Integration Guide | //| P0-1: ATOMIC CSV WRITES - Production-hardened with file locking | //| - Checksum validation per row | //| - Atomic write operations (write to temp, then rename) | //| - Automatic corruption detection and recovery | //| - File locking during writes to prevent race conditions | //+------------------------------------------------------------------+ #ifndef CSHADOWLOGGER_MQH #define CSHADOWLOGGER_MQH #include #include "CGateBase.mqh" //+------------------------------------------------------------------+ //| Checksum Calculation - Simple but effective | //+------------------------------------------------------------------+ uint CalculateChecksum(string data) { uint checksum = 0; for(int i = 0; i < StringLen(data); i++) { checksum = ((checksum << 5) + checksum) + (uchar)StringGetCharacter(data, i); } return checksum; } //+------------------------------------------------------------------+ //| Shadow Log Entry Structure | //+------------------------------------------------------------------+ struct SShadowLogEntry { datetime timestamp; string symbol; string direction; // BUY/SELL double price; double volume; string strategy_name; // Gate results for all 8 gates bool gate_passed[8]; double gate_confidence[8]; double gate_threshold[8]; string gate_block_reason[8]; ulong gate_latency[8]; // Overall result bool all_gates_passed; double final_confidence; string execution_status; // "EXECUTED", "SHADOW", "BLOCKED" // Trade outcome (filled later on position close) double actual_pnl; datetime close_time; double close_price; // P0-1: Checksum for data integrity validation uint row_checksum; void Init(string sym, string dir, double pr, double vol, string strat) { timestamp = TimeCurrent(); symbol = sym; direction = dir; price = pr; volume = vol; strategy_name = strat; all_gates_passed = false; final_confidence = 0.0; execution_status = "PENDING"; actual_pnl = 0.0; close_time = 0; close_price = 0.0; row_checksum = 0; ArrayInitialize(gate_passed, false); ArrayInitialize(gate_confidence, 0.0); ArrayInitialize(gate_threshold, 0.0); for(int i=0; i<8; i++) gate_block_reason[i] = ""; } void SetGateResult(int gate_num, const EGateResult &result) { if(gate_num < 0 || gate_num >= 8) return; gate_passed[gate_num] = result.passed; gate_confidence[gate_num] = result.confidence; gate_threshold[gate_num] = result.threshold; gate_block_reason[gate_num] = result.block_reason; gate_latency[gate_num] = result.latency_us; } // P0-1: Calculate checksum for the row data void CalculateRowChecksum() { string data = BuildChecksumString(); row_checksum = CalculateChecksum(data); } // P0-1: Build string for checksum calculation string BuildChecksumString() const { return StringFormat("%s%s%s%.5f%.2f%s%d%.4f", TimeToString(timestamp, TIME_DATE|TIME_SECONDS), symbol, direction, price, volume, strategy_name, all_gates_passed ? 1 : 0, final_confidence); } // P0-1: Validate checksum bool ValidateChecksum() const { string data = BuildChecksumString(); return row_checksum == CalculateChecksum(data); } }; //+------------------------------------------------------------------+ //| Shadow Logger Class | //+------------------------------------------------------------------+ class CShadowLogger { private: string m_filename; string m_temp_filename; // P0-1: Temp file for atomic writes string m_lock_filename; // P0-1: Lock file for synchronization bool m_initialized; bool m_shadow_mode; int m_entry_count; string m_log_directory; // P0-1: File locking state bool m_is_locked; int m_lock_handle; // Ring buffer for recent entries (for quick access) SShadowLogEntry m_recent_entries[]; int m_recent_capacity; int m_recent_index; // P0-1: Write retry configuration int m_max_retries; int m_retry_delay_ms; // P0-1: Memory buffer fallback when disk I/O fails string m_memory_buffer[]; int m_memory_buffer_count; int m_memory_buffer_max; bool m_is_tester; // Tester mode detection public: // Constructor CShadowLogger() : m_initialized(false), m_shadow_mode(false), m_entry_count(0), m_recent_capacity(100), m_recent_index(0), m_is_locked(false), m_lock_handle(INVALID_HANDLE), m_max_retries(3), m_retry_delay_ms(10), m_memory_buffer_count(0), m_memory_buffer_max(1000), m_is_tester(false) // Will be detected in Initialize() { ArrayResize(m_recent_entries, m_recent_capacity); ArrayResize(m_memory_buffer, m_memory_buffer_max); m_log_directory = "DualEA\\ShadowLogs\\"; } // Destructor ~CShadowLogger() { // P0-1: Ensure lock is released ReleaseFileLock(); } //+------------------------------------------------------------------+ //| P0-1: Acquire file lock using lock file | //+------------------------------------------------------------------+ bool AcquireFileLock() { if(m_is_locked) return true; // Try to create lock file (exclusive access) m_lock_handle = FileOpen(m_lock_filename, FILE_WRITE|FILE_TXT|FILE_COMMON); if(m_lock_handle == INVALID_HANDLE) { // Lock file exists, another process has the lock return false; } // Write lock metadata FileWriteString(m_lock_handle, StringFormat("PID=%d|TIME=%s", GetTickCount(), TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS))); FileFlush(m_lock_handle); m_is_locked = true; return true; } //+------------------------------------------------------------------+ //| P0-1: Release file lock | //+------------------------------------------------------------------+ void ReleaseFileLock() { if(!m_is_locked) return; if(m_lock_handle != INVALID_HANDLE) { FileClose(m_lock_handle); m_lock_handle = INVALID_HANDLE; } // Delete lock file FileDelete(m_lock_filename, FILE_COMMON); m_is_locked = false; } //+------------------------------------------------------------------+ //| P0-1: Wait for and acquire lock with timeout | //+------------------------------------------------------------------+ bool WaitForLock(int timeout_ms=5000) { int elapsed = 0; while(elapsed < timeout_ms) { if(AcquireFileLock()) return true; // Wait before retry Sleep(m_retry_delay_ms); elapsed += m_retry_delay_ms; } Print("[ShadowLogger] WARNING: Could not acquire file lock within timeout"); return false; } //+------------------------------------------------------------------+ //| P0-1: Atomic file write (efficient append with retry) | //+------------------------------------------------------------------+ bool AtomicWrite(const string data) { // TESTER MODE: Skip all file I/O to prevent crashes if(m_is_tester) { // Just count entries, no actual file writing m_entry_count++; return true; } // Fast path: skip locking in tester mode or when file doesn't exist yet bool need_lock = (m_entry_count > 10) && !m_shadow_mode; if(need_lock && !m_is_locked && !WaitForLock(500)) { // In tester mode or when lock fails, proceed without lock but with retries Print("[ShadowLogger] INFO: Proceeding without file lock (tester mode or lock unavailable)"); } bool success = false; int retries = 0; while(!success && retries < m_max_retries) { // Efficient append: open file for writing at end (no read required) int handle = FileOpen(m_filename, FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_SHARE_READ, ','); if(handle == INVALID_HANDLE) { retries++; Sleep(m_retry_delay_ms * retries); // Progressive backoff: 10ms, 20ms, 30ms continue; } // Seek to end (in case file wasn't opened in append mode) FileSeek(handle, 0, SEEK_END); // Write data with newline FileWriteString(handle, data + "\n"); FileFlush(handle); FileClose(handle); // Quick verification: just check file exists and grew if(FileIsExist(m_filename, FILE_COMMON)) { success = true; } else { retries++; Print("[ShadowLogger] WARNING: Write verification failed, retrying..."); Sleep(m_retry_delay_ms * retries); } } // Release lock if we acquired it if(m_is_locked) ReleaseFileLock(); // If disk write failed, buffer to memory as fallback if(!success) { if(m_memory_buffer_count < m_memory_buffer_max) { m_memory_buffer[m_memory_buffer_count] = data; m_memory_buffer_count++; Print(StringFormat("[ShadowLogger] WARNING: Disk write failed - buffered to memory (%d items)", m_memory_buffer_count)); } else { Print("[ShadowLogger] ERROR: Disk write failed AND memory buffer full - data loss imminent"); } } return success; } //+------------------------------------------------------------------+ //| P0-1: Flush memory buffer to disk (call from OnTimer) | //+------------------------------------------------------------------+ void FlushMemoryBuffer() { if(m_memory_buffer_count == 0) return; int flushed = 0; for(int i = 0; i < m_memory_buffer_count && i < 100; i++) // Max 100 per flush { // Use simple write (no verification needed for buffered data) int handle = FileOpen(m_filename, FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_SHARE_READ, ','); if(handle == INVALID_HANDLE) break; FileSeek(handle, 0, SEEK_END); FileWriteString(handle, m_memory_buffer[0] + "\n"); FileFlush(handle); FileClose(handle); // Remove from buffer by shifting for(int j = 0; j < m_memory_buffer_count - 1; j++) m_memory_buffer[j] = m_memory_buffer[j + 1]; m_memory_buffer_count--; flushed++; } if(flushed > 0) Print(StringFormat("[ShadowLogger] Flushed %d entries from memory buffer (%d remaining)", flushed, m_memory_buffer_count)); } //+------------------------------------------------------------------+ //| P0-1: Check if memory buffer has entries | //+------------------------------------------------------------------+ int GetMemoryBufferCount() const { return m_memory_buffer_count; } //+------------------------------------------------------------------+ //| P0-1: Verify last write was successful | //+------------------------------------------------------------------+ bool VerifyLastWrite(const string expected_data) { int verify_handle = FileOpen(m_filename, FILE_READ|FILE_CSV|FILE_COMMON, ','); if(verify_handle == INVALID_HANDLE) return false; // Read last line string last_line = ""; string current_line = ""; while(!FileIsEnding(verify_handle)) { current_line = FileReadString(verify_handle); if(StringLen(current_line) > 0) { last_line = current_line; } } FileClose(verify_handle); // Check if last line matches expected data return (last_line == expected_data); } //+------------------------------------------------------------------+ //| P0-1: Detect and recover from corruption | //+------------------------------------------------------------------+ bool DetectAndRecoverCorruption() { Print("[ShadowLogger] Checking for file corruption..."); // Open as TEXT file to read line by line (not CSV field by field) int handle = FileOpen(m_filename, FILE_READ|FILE_TXT|FILE_COMMON); if(handle == INVALID_HANDLE) return false; int corrupted_rows = 0; int total_rows = 0; string valid_content = ""; bool header_written = false; // Read file line by line while(!FileIsEnding(handle)) { string line = FileReadString(handle); total_rows++; // Skip empty lines if(StringLen(line) == 0) { corrupted_rows++; continue; } // Check if this looks like header row (case-insensitive check) string line_lower = line; // Simple lowercase conversion for comparison for(int i = 0; i < StringLen(line_lower); i++) { ushort c = StringGetCharacter(line_lower, i); if(c >= 'A' && c <= 'Z') StringSetCharacter(line_lower, i, (ushort)(c + 32)); } if(StringFind(line_lower, "timestamp") == 0) { valid_content += line + "\n"; header_written = true; continue; } // Validate data row: check for proper CSV structure string parts[]; int count = StringSplit(line, ',', parts); // Expected: timestamp, symbol, direction, price, volume, strategy, // 8 gates * 5 fields each, all_passed, final_conf, status, pnl, close_time, close_price, checksum // = 6 + 40 + 6 = 52 fields minimum if(count >= 6) // At least basic trade info { valid_content += line + "\n"; } else { corrupted_rows++; Print(StringFormat("[ShadowLogger] Corrupted row %d detected (only %d fields) and skipped: %.50s", total_rows, count, line)); } } FileClose(handle); if(corrupted_rows > 0) { Print(StringFormat("[ShadowLogger] Recovered from corruption: %d/%d rows valid", total_rows - corrupted_rows, total_rows)); // Write recovered content back int recover_handle = FileOpen(m_filename, FILE_WRITE|FILE_TXT|FILE_COMMON); if(recover_handle != INVALID_HANDLE) { FileWriteString(recover_handle, valid_content); FileClose(recover_handle); return true; } } return (corrupted_rows == 0); } //+------------------------------------------------------------------+ //| Initialize logger | //+------------------------------------------------------------------+ bool Initialize(bool shadow_mode=false, string custom_filename="") { m_shadow_mode = shadow_mode; // Detect tester mode - skip file operations in tester to prevent crashes m_is_tester = (MQLInfoInteger(MQL_TESTER) != 0); if(m_is_tester) { // In tester mode, just mark as initialized but don't create files m_initialized = true; Print("[ShadowLogger] TESTER MODE: File logging disabled (memory-only mode)"); return true; } // Generate filename with timestamp if(custom_filename == "") { datetime now = TimeCurrent(); MqlDateTime dt; TimeToStruct(now, dt); m_filename = StringFormat("%s%s_shadow_%04d%02d%02d_%02d%02d.csv", m_log_directory, shadow_mode ? "shadow" : "live", dt.year, dt.mon, dt.day, dt.hour, dt.min); } else { m_filename = m_log_directory + custom_filename; } // P0-1: Set up temp and lock filenames m_temp_filename = m_filename + ".tmp"; m_lock_filename = m_filename + ".lock"; // P0-1: Check for and recover from corruption on startup if(FileIsExist(m_filename, FILE_COMMON)) { DetectAndRecoverCorruption(); } // Write CSV header int handle = FileOpen(m_filename, FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_SHARE_READ, ','); if(handle == INVALID_HANDLE) { Print("[ShadowLogger] ERROR: Failed to create log file: " + m_filename); return false; } // Write comprehensive header string header = "timestamp," + "symbol," + "direction," + "price," + "volume," + "strategy_name," + "g1_passed,g1_conf,g1_thresh,g1_reason,g1_latency," + "g2_passed,g2_conf,g2_thresh,g2_reason,g2_latency," + "g3_passed,g3_conf,g3_thresh,g3_reason,g3_latency," + "g4_passed,g4_conf,g4_thresh,g4_reason,g4_latency," + "g5_passed,g5_conf,g5_thresh,g5_reason,g5_latency," + "g6_passed,g6_conf,g6_thresh,g6_reason,g6_latency," + "g7_passed,g7_conf,g7_thresh,g7_reason,g7_latency," + "g8_passed,g8_conf,g8_thresh,g8_reason,g8_latency," + "all_passed," + "final_conf," + "execution_status," + "pnl," + "close_time," + "close_price," + "checksum"; // P0-1: Add checksum column FileWriteString(handle, header + "\n"); FileClose(handle); m_initialized = true; Print(StringFormat("[ShadowLogger] Initialized: %s (Mode: %s, P0-1 Atomic Writes: ENABLED)", m_filename, shadow_mode ? "SHADOW" : "LIVE")); return true; } //+------------------------------------------------------------------+ //| Log a gate decision entry | //+------------------------------------------------------------------+ void LogGateDecision(SShadowLogEntry &entry) // Note: non-const to allow checksum calc { if(!m_initialized) return; // P0-1: Calculate checksum before writing entry.CalculateRowChecksum(); // Build CSV line string line = BuildCSVLine(entry); // P0-1: Use atomic write operation if(!AtomicWrite(line)) { Print("[ShadowLogger] ERROR: Atomic write failed after retries"); return; } // Add to recent buffer AddToRecentBuffer(entry); m_entry_count++; } //+------------------------------------------------------------------+ //| Log trade outcome (update existing entry) | //+------------------------------------------------------------------+ void LogTradeOutcome(datetime entry_time, string symbol, double pnl, datetime close_time, double close_price) { if(!m_initialized) return; // Note: In a production system, you'd maintain an index file // to quickly locate entries for updating. For now, we append outcome // as a separate record or rely on the recent buffer. // Find in recent buffer for(int i=0; i 0 ? TimeToString(entry.close_time, TIME_DATE|TIME_SECONDS) : "") + "," + DoubleToString(entry.close_price, 5) + "," + IntegerToString((long)entry.row_checksum); // P0-1: Add checksum return line; } //+------------------------------------------------------------------+ //| Add to recent buffer (ring buffer) | //+------------------------------------------------------------------+ void AddToRecentBuffer(const SShadowLogEntry &entry) { m_recent_entries[m_recent_index] = entry; m_recent_index = (m_recent_index + 1) % m_recent_capacity; } //+------------------------------------------------------------------+ //| Getters | //+------------------------------------------------------------------+ bool IsShadowMode() const { return m_shadow_mode; } bool IsInitialized() const { return m_initialized; } string GetFilename() const { return m_filename; } int GetEntryCount() const { return m_entry_count; } //+------------------------------------------------------------------+ //| Get recent entries for analysis | //+------------------------------------------------------------------+ void GetRecentEntries(SShadowLogEntry &entries[], int count) { int actual_count = MathMin(count, MathMin(m_entry_count, m_recent_capacity)); ArrayResize(entries, actual_count); for(int i=0; i 0) wins++; else { losses++; loss_pnl += MathAbs(m_recent_entries[i].actual_pnl); } } } total_trades = wins + losses; if(total_trades > 0) { win_rate = (double)wins / total_trades; avg_confidence = conf_sum / total_trades; profit_factor = loss_pnl > 0 ? total_pnl / loss_pnl : (total_pnl > 0 ? 999.0 : 0.0); } else { win_rate = 0.0; profit_factor = 0.0; avg_confidence = 0.0; } } //+------------------------------------------------------------------+ //| Print summary statistics | //+------------------------------------------------------------------+ void PrintStats() { double wr, pf, ac; int trades; CalculateRecentStats(wr, pf, ac, trades); Print(StringFormat("[ShadowLogger] Stats: %d entries, Recent: %d trades, WR=%.1f%%, PF=%.2f, AvgConf=%.3f", m_entry_count, trades, wr*100, pf, ac)); } }; #endif // CSHADOWLOGGER_MQH