dualea/Experts/Advisors/DualEA/Tests/Test_TradeLoggerContract.mq5
2026-10-28 00:31:51 -04:00
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; }