771 lines
26 KiB
MQL5
771 lines
26 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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|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<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
|