124 lines
5.1 KiB
MQL5
#property strict
| |||
#include "../Include/UnifiedTradeLogger.mqh"
| |||
| |||
int checks = 0;
| |||
int failures = 0;
| |||
void Check(bool passed, string label)
| |||
{
| |||
checks++;
| |||
if(!passed) { failures++; Print("[LOGGER FAIL] ", label); }
| |||
}
| |||
| |||
string ReadLog(string path)
| |||
{
| |||
int file = FileOpen(path, FILE_READ|FILE_TXT|FILE_ANSI|DUALEA_FILE_COMMON, 0, CP_UTF8);
| |||
if(file == INVALID_HANDLE) return "";
| |||
string result = "";
| |||
while(!FileIsEnding(file)) result += FileReadString(file) + "\n";
| |||
FileClose(file);
| |||
return result;
| |||
}
| |||
| |||
int Occurrences(string text, string token)
| |||
{
| |||
int count=0, pos=0;
| |||
while((pos=StringFind(text, token, pos)) >= 0) { count++; pos+=StringLen(token); }
| |||
return count;
| |||
}
| |||
| |||
bool BalancedJSON(string text)
| |||
{
| |||
bool quoted=false, escaped=false;
| |||
string stack="";
| |||
for(int i=0; i<StringLen(text); i++)
| |||
{
| |||
ushort ch=StringGetCharacter(text,i);
| |||
if(quoted)
| |||
{
| |||
if(escaped) escaped=false;
| |||
else if(ch==92) escaped=true;
| |||
else if(ch==34) quoted=false;
| |||
else if(ch<32) return false;
| |||
continue;
| |||
}
| |||
if(ch==34) quoted=true;
| |||
else if(ch==123 || ch==91) stack+=ShortToString(ch);
| |||
else if(ch==125 || ch==93)
| |||
{
| |||
int size=StringLen(stack);
| |||
if(size==0) return false;
| |||
ushort opening=StringGetCharacter(stack,size-1);
| |||
if((ch==125 && opening!=123) || (ch==93 && opening!=91)) return false;
| |||
stack=StringSubstr(stack,0,size-1);
| |||
}
| |||
}
| |||
StringTrimLeft(text);
| |||
StringTrimRight(text);
| |||
return !quoted && stack=="" && StringLen(text)>1 && StringGetCharacter(text,0)==123 && StringGetCharacter(text,StringLen(text)-1)==125;
| |||
}
| |||
| |||
int OnInit()
| |||
{
| |||
if(!MQLInfoInteger(MQL_TESTER)) return INIT_FAILED;
| |||
// Each run uses its own tester-local directory, never the live trade log.
| |||
string directory="DualEA\\logger_contract_"+IntegerToString((long)GetTickCount64());
| |||
CUnifiedTradeLogger logger(directory);
| |||
UnifiedTradeRecord record;
| |||
Check(record.journey_length == 0, "new record has an empty journey");
| |||
Check(record.blocking_gate == -1, "new record has no blocking gate");
| |||
Check(record.reviews_performed == 0 && !record.is_closed, "new lifecycle state is initialized");
| |||
string json = logger.FormatTradeAsJSON(record, false);
| |||
Check(StringFind(json, "\"gate\":") < 0, "empty journey formats without indexing entries");
| |||
| |||
for(int i=0; i<ArraySize(record.gate_journey); i++)
| |||
{
| |||
record.gate_journey[i].gate_name = "test_" + IntegerToString(i);
| |||
record.gate_journey[i].gate_index = i;
| |||
}
| |||
record.journey_length = 1;
| |||
json = logger.FormatTradeAsJSON(record, false);
| |||
Check(StringFind(json, "test_0") >= 0 && StringFind(json, "test_1") < 0, "one-step journey stays one step");
| |||
record.journey_length = 2147483647;
| |||
json = logger.FormatTradeAsJSON(record, false);
| |||
Check(StringFind(json, "test_23") >= 0, "oversized journey clamps to fixed capacity");
| |||
record.journey_length = -1;
| |||
json = logger.FormatTradeAsJSON(record, true);
| |||
Check(StringFind(json, "\"gate\":") < 0, "negative journey formats as empty");
| |||
Check(StringFind(json, "\"outcome\":") >= 0, "close formatting remains available");
| |||
| |||
MqlDateTime dt;
| |||
TimeToStruct(TimeCurrent(),dt);
| |||
string path=directory+"\\trades_"+StringFormat("%04d%02d%02d",dt.year,dt.mon,dt.day)+".json";
| |||
record.journey_length=1;
| |||
record.trade_id="trade-a";
| |||
record.execution_time=TimeCurrent();
| |||
record.strategy_name="quoted \"strategy\" with \\ slash\nbrace }";
| |||
logger.LogTradeExecution(record);
| |||
string first=ReadLog(path);
| |||
Check(BalancedJSON(first) && Occurrences(first,"\"trade_id\":")==1, "first write is a complete document");
| |||
record.trade_id="trade-b";
| |||
logger.LogTradeExecution(record);
| |||
string second=ReadLog(path);
| |||
Check(BalancedJSON(second) && Occurrences(second,"\"trade_id\":")==2, "second append preserves both records and balanced arrays");
| |||
Check(Occurrences(second,"\"status\": \"open\"")==2, "both executions are open");
| |||
Check(StringFind(second,"quoted \\\"strategy\\\" with \\\\ slash\\nbrace }")>=0, "string quotes, slashes and newlines are escaped");
| |||
record.trade_id="trade-a";
| |||
record.close_time=TimeCurrent();
| |||
record.profit_absolute=12.5;
| |||
logger.LogTradeClose(record);
| |||
string closed=ReadLog(path);
| |||
Check(BalancedJSON(closed) && Occurrences(closed,"\"trade_id\":")==2, "closing preserves two complete records");
| |||
Check(Occurrences(closed,"\"status\": \"closed\"")==1 && Occurrences(closed,"\"status\": \"open\"")==1, "only matching trade is closed");
| |||
Check(Occurrences(closed,"\"outcome\":")==1 && StringFind(closed,"\"profit_absolute\": 12.50")>=0, "close outcome persisted");
| |||
int logged, total_closed;
| |||
logger.GetStatistics(logged,total_closed);
| |||
Check(logged==2 && total_closed==1, "statistics reflect committed writes");
| |||
record.trade_id="missing";
| |||
logger.LogTradeClose(record);
| |||
Check(ReadLog(path)==closed, "unknown close leaves log unchanged");
| |||
Print("[LOGGER ARTIFACT] ",path);
| |||
PrintFormat("[LOGGER-CONTRACT] RESULT checks=%d failures=%d", checks, failures);
| |||
return failures == 0 ? INIT_SUCCEEDED : INIT_FAILED;
| |||
}
| |||
void OnTick() { ExpertRemove(); }
| |||
double OnTester() { return failures == 0 ? checks : -failures; }
|