| #include "RuntimeStorage.mqh"
|
| //+------------------------------------------------------------------+
|
| //| 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 <Files/File.mqh>
|
| #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|DUALEA_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, DUALEA_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|DUALEA_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, DUALEA_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|DUALEA_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|DUALEA_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|DUALEA_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|DUALEA_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, DUALEA_FILE_COMMON))
|
| {
|
| DetectAndRecoverCorruption();
|
| }
|
|
|
| // Write CSV header
|
| int handle = FileOpen(m_filename,
|
| FILE_WRITE|FILE_CSV|DUALEA_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<m_recent_capacity; i++)
|
| {
|
| int idx = (m_recent_index - 1 - i + m_recent_capacity) % m_recent_capacity;
|
| if(m_recent_entries[idx].timestamp == entry_time &&
|
| m_recent_entries[idx].symbol == symbol)
|
| {
|
| m_recent_entries[idx].actual_pnl = pnl;
|
| m_recent_entries[idx].close_time = close_time;
|
| m_recent_entries[idx].close_price = close_price;
|
| break;
|
| }
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Build CSV line from entry |
|
| //+------------------------------------------------------------------+
|
| string BuildCSVLine(const SShadowLogEntry &entry)
|
| {
|
| string line =
|
| TimeToString(entry.timestamp, TIME_DATE|TIME_SECONDS) + "," +
|
| entry.symbol + "," +
|
| entry.direction + "," +
|
| DoubleToString(entry.price, 5) + "," +
|
| DoubleToString(entry.volume, 2) + "," +
|
| entry.strategy_name;
|
|
|
| // Add all 8 gate results
|
| for(int i=0; i<8; i++)
|
| {
|
| line += "," +
|
| (entry.gate_passed[i] ? "1" : "0") + "," +
|
| DoubleToString(entry.gate_confidence[i], 4) + "," +
|
| DoubleToString(entry.gate_threshold[i], 4) + "," +
|
| entry.gate_block_reason[i] + "," +
|
| IntegerToString((long)entry.gate_latency[i]);
|
| }
|
|
|
| // Add overall result
|
| line += "," +
|
| (entry.all_gates_passed ? "1" : "0") + "," +
|
| DoubleToString(entry.final_confidence, 4) + "," +
|
| entry.execution_status + "," +
|
| DoubleToString(entry.actual_pnl, 2) + "," +
|
| (entry.close_time > 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<actual_count; i++)
|
| {
|
| int idx = (m_recent_index - 1 - i + m_recent_capacity) % m_recent_capacity;
|
| entries[i] = m_recent_entries[idx];
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Calculate statistics from recent entries |
|
| //+------------------------------------------------------------------+
|
| void CalculateRecentStats(double &win_rate, double &profit_factor,
|
| double &avg_confidence, int &total_trades)
|
| {
|
| int count = MathMin(m_entry_count, m_recent_capacity);
|
| if(count == 0)
|
| {
|
| win_rate = 0.0;
|
| profit_factor = 0.0;
|
| avg_confidence = 0.0;
|
| total_trades = 0;
|
| return;
|
| }
|
|
|
| int wins = 0, losses = 0;
|
| double total_pnl = 0, loss_pnl = 0, conf_sum = 0;
|
|
|
| for(int i=0; i<count; i++)
|
| {
|
| if(m_recent_entries[i].actual_pnl != 0)
|
| {
|
| total_pnl += m_recent_entries[i].actual_pnl;
|
| conf_sum += m_recent_entries[i].final_confidence;
|
|
|
| if(m_recent_entries[i].actual_pnl > 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
|