2025-08-13 19:41:57 -04:00 | | | // #property script_show_inputs // disabled for headless runs (kb_check)
|
| | | #property strict
|
| | |
|
| | | // DualEA Scripts/ValidateInsights.mq5
|
| | | // Validates DualEA\insights.json schema presence and slice coverage in Common Files.
|
| | | // Also checks staleness against features.csv and knowledge_base.csv.
|
| | | // Writes a compact report to DualEA\insights_validation.txt
|
| | |
|
| | | input string RequireSymbolsCSV = ""; // e.g. "USDCNH,EURUSD"
|
| | | input string RequireTimeframesCSV = ""; // e.g. "10,15,60"
|
| | | input string RequireStrategiesCSV = ""; // e.g. "BollAverages,DonchianATRBreakout"
|
| | | input bool FailOnStale = false; // non-zero return code via Print/Alert pattern
|
| | | input bool FailOnEmptySlices = false; // non-zero return code via Print/Alert pattern
|
| | | input bool WriteReport = true;
|
| | | input string ReportFile = "DualEA\\insights_validation.txt"; // Common Files
|
| | | input bool FailOnMissingRequired = false; // fail if any required symbol/strategy/timeframe missing
|
| | | input string RequirementsConfig = "DualEA\\validate_requirements.txt"; // Common Files config
|
| | |
|
| | | // --- small utils ---
|
| | | string TrimCopy(const string s)
|
| | | {
|
| | | string r = s;
|
| | | StringTrimLeft(r);
|
| | | StringTrimRight(r);
|
| | | return r;
|
| | | }
|
| | | string StripQuotes(const string s)
|
| | | {
|
| | | string r = TrimCopy(s);
|
| | | int n = StringLen(r);
|
| | | if(n>=2 && StringGetCharacter(r,0)=='"' && StringGetCharacter(r,n-1)=='"')
|
| | | return StringSubstr(r,1,n-2);
|
| | | return r;
|
| | | }
|
| | | void PushUniqueS(string &arr[], const string v)
|
| | | {
|
| | | for(int i=0;i<ArraySize(arr);++i) if(arr[i]==v) return;
|
| | | int k=ArraySize(arr); ArrayResize(arr,k+1); arr[k]=v;
|
| | | }
|
| | | void PushUniqueI(int &arr[], const int v)
|
| | | {
|
| | | for(int i=0;i<ArraySize(arr);++i) if(arr[i]==v) return;
|
| | | int k=ArraySize(arr); ArrayResize(arr,k+1); arr[k]=v;
|
| | | }
|
| | | void SplitCSVList(const string csv, string &out[])
|
| | | {
|
| | | ArrayResize(out,0);
|
| | | if(StringLen(TrimCopy(csv))==0) return;
|
| | | ushort sep = (ushort)StringGetCharacter(",",0);
|
| | | string parts[]; int cnt = StringSplit(csv, sep, parts);
|
| | | for(int i=0;i<cnt;++i)
|
| | | {
|
| | | string t = TrimCopy(parts[i]);
|
| | | if(StringLen(t)>0) PushUniqueS(out, t);
|
| | | }
|
| | | }
|
| | |
|
| | | string JoinStrings(const string &arr[], const string sep)
|
| | | {
|
| | | string out="";
|
| | | for(int i=0;i<ArraySize(arr);++i){ if(i>0) out+=sep; out+=arr[i]; }
|
| | | return out;
|
| | | }
|
| | | string JoinInts(const int &arr[], const string sep)
|
| | | {
|
| | | string out="";
|
| | | for(int i=0;i<ArraySize(arr);++i){ if(i>0) out+=sep; out+=IntegerToString(arr[i]); }
|
| | | return out;
|
| | | }
|
| | |
|
| | | void LoadRequirementsFromConfig(const string path,
|
| | | string &symsCSV,
|
| | | string &tfsCSV,
|
| | | string &stratsCSV,
|
| | | bool &failMissing)
|
| | | {
|
| | | int h = FileOpen(path, FILE_READ|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
| | | if(h==INVALID_HANDLE) return;
|
| | | while(!FileIsEnding(h))
|
| | | {
|
| | | string line = FileReadString(h);
|
| | | if(line=="" && FileIsEnding(h)) break;
|
| | | string t = TrimCopy(line);
|
| | | if(StringLen(t)==0) continue;
|
| | | if(StringGetCharacter(t,0)=='#') continue;
|
| | | int eq = StringFind(t, "=", 0);
|
| | | if(eq<0) continue;
|
| | | string key = TrimCopy(StringSubstr(t,0,eq));
|
| | | string val = TrimCopy(StringSubstr(t,eq+1));
|
| | | if(key=="RequireSymbolsCSV") symsCSV = val;
|
| | | else if(key=="RequireTimeframesCSV") tfsCSV = val;
|
| | | else if(key=="RequireStrategiesCSV") stratsCSV = val;
|
| | | else if(key=="FailOnMissingRequired")
|
| | | {
|
2025-09-12 20:46:11 -04:00 | | | failMissing = (val=="1" || val=="true" || val=="yes" || val=="y");
|
2025-08-13 19:41:57 -04:00 | | | }
|
| | | }
|
| | | FileClose(h);
|
| | | }
|
| | |
|
| | | int CountOccurrencesInSection(const string path, const string section_key, const string field_key)
|
| | | {
|
| | | int h = FileOpen(path, FILE_READ|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
| | | if(h==INVALID_HANDLE) return 0;
|
| | | bool in_section=false; int count=0;
|
| | | while(!FileIsEnding(h))
|
| | | {
|
| | | string line = FileReadString(h);
|
| | | if(line=="" && FileIsEnding(h)) break;
|
| | | if(StringFind(line, section_key, 0)>=0) { in_section=true; continue; }
|
| | | if(in_section)
|
| | | {
|
| | | if(StringFind(line, "]", 0)>=0) { in_section=false; continue; }
|
| | | if(StringFind(line, field_key, 0)>=0) count++;
|
| | | }
|
| | | }
|
| | | FileClose(h);
|
| | | return count;
|
| | | }
|
| | |
|
| | | void Validate()
|
| | | {
|
| | | string INS = "DualEA\\insights.json";
|
| | | string FEAT= "DualEA\\features.csv";
|
| | | string KB = "DualEA\\knowledge_base.csv";
|
| | |
|
| | | bool ins_exists = (FileGetInteger(INS, FILE_EXISTS, true)>0);
|
| | | bool feat_exists= (FileGetInteger(FEAT, FILE_EXISTS, true)>0);
|
| | | bool kb_exists = (FileGetInteger(KB, FILE_EXISTS, true)>0);
|
| | |
|
| | | if(!ins_exists)
|
| | | {
|
| | | Print("[VALIDATE] insights.json: NOT FOUND in Common Files/DualEA");
|
| | | return;
|
| | | }
|
| | |
|
| | | datetime t_ins = (datetime)FileGetInteger(INS, FILE_MODIFY_DATE, true);
|
| | | datetime t_feat= (feat_exists? (datetime)FileGetInteger(FEAT, FILE_MODIFY_DATE, true) : 0);
|
| | | datetime t_kb = (kb_exists? (datetime)FileGetInteger(KB, FILE_MODIFY_DATE, true) : 0);
|
| | | bool is_stale = (feat_exists && t_feat>t_ins) || (kb_exists && t_kb>t_ins);
|
| | |
|
| | | // Totals presence check
|
| | | int h = FileOpen(INS, FILE_READ|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
| | | if(h==INVALID_HANDLE)
|
| | | {
|
| | | Print("[VALIDATE] ERROR: cannot open insights.json (encoding?)");
|
| | | return;
|
| | | }
|
| | | bool has_schema=false, has_generated=false, has_totals=false;
|
| | | int total_trade_count=-1;
|
| | | while(!FileIsEnding(h))
|
| | | {
|
| | | string line = FileReadString(h);
|
| | | if(line=="" && FileIsEnding(h)) break;
|
| | | if(StringFind(line, "schema_version", 0)>=0) has_schema=true;
|
| | | if(StringFind(line, "generated_at", 0)>=0) has_generated=true;
|
| | | if(StringFind(line, "totals", 0)>=0) has_totals=true;
|
| | | int p = StringFind(line, "trade_count", 0);
|
| | | if(p>=0)
|
| | | {
|
| | | int c = StringFind(line, ":", p);
|
| | | if(c>=0) total_trade_count = (int)StringToInteger(TrimCopy(StringSubstr(line, c+1)));
|
| | | }
|
| | | }
|
| | | FileClose(h);
|
| | |
|
| | | // Section counts
|
| | | int cnt_by_strategy = CountOccurrencesInSection(INS, "by_strategy", "strategy");
|
| | | int cnt_by_tf = CountOccurrencesInSection(INS, "by_timeframe", "timeframe");
|
| | |
|
| | | // Slice coverage with uniques
|
| | | int hs = FileOpen(INS, FILE_READ|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
| | | bool in_slice=false; string cur_s="", cur_y=""; int cur_tf=-1; int slice_count=0;
|
| | | string uniq_syms[]; string uniq_strats[]; int uniq_tfs[];
|
| | | if(hs!=INVALID_HANDLE)
|
| | | {
|
| | | while(!FileIsEnding(hs))
|
| | | {
|
| | | string line = FileReadString(hs);
|
| | | if(line=="" && FileIsEnding(hs)) break;
|
| | | if(StringFind(line, "by_symbol_strategy_timeframe", 0)>=0) { in_slice=true; continue; }
|
| | | if(in_slice)
|
| | | {
|
| | | if(StringFind(line, "]", 0)>=0) { in_slice=false; continue; }
|
| | | int p;
|
| | | // Robust extraction: find ':' after key, then take the next quoted value
|
| | | int ps = StringFind(line, "strategy", 0);
|
| | | if(ps>=0)
|
| | | {
|
| | | int colon = StringFind(line, ":", ps);
|
| | | if(colon>=0)
|
| | | {
|
| | | int q1 = StringFind(line, "\"", colon);
|
| | | int q2 = StringFind(line, "\"", q1+1);
|
| | | if(q1>=0 && q2>q1) cur_s = StringSubstr(line, q1+1, q2-q1-1);
|
| | | }
|
| | | }
|
| | | int py = StringFind(line, "symbol", 0);
|
| | | if(py>=0)
|
| | | {
|
| | | int colon2 = StringFind(line, ":", py);
|
| | | if(colon2>=0)
|
| | | {
|
| | | int q1 = StringFind(line, "\"", colon2);
|
| | | int q2 = StringFind(line, "\"", q1+1);
|
| | | if(q1>=0 && q2>q1) cur_y = StringSubstr(line, q1+1, q2-q1-1);
|
| | | }
|
| | | }
|
| | | p = StringFind(line, "timeframe", 0);
|
| | | if(p>=0)
|
| | | {
|
| | | int c = StringFind(line, ":", p); if(c>=0){ string num = TrimCopy(StringSubstr(line, c+1)); cur_tf = (int)StringToInteger(num); }
|
| | | }
|
| | | // Commit object when we see closing brace on its line
|
| | | if(StringFind(line, "}", 0)>=0)
|
| | | {
|
| | | if(cur_s!="" && cur_y!="" && cur_tf!=-1)
|
| | | {
|
| | | slice_count++;
|
| | | PushUniqueS(uniq_syms, cur_y);
|
| | | PushUniqueS(uniq_strats, cur_s);
|
| | | PushUniqueI(uniq_tfs, cur_tf);
|
| | | }
|
| | | cur_s=""; cur_y=""; cur_tf=-1;
|
| | | }
|
| | | }
|
| | | }
|
| | | FileClose(hs);
|
| | | }
|
| | |
|
| | | // Fallback: if parser failed (e.g., formatting nuances), fall back to simple token counting inside section
|
| | | if(slice_count==0)
|
| | | {
|
| | | int hf = FileOpen(INS, FILE_READ|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
| | | bool in_section=false;
|
| | | if(hf!=INVALID_HANDLE)
|
| | | {
|
| | | while(!FileIsEnding(hf))
|
| | | {
|
| | | string line = FileReadString(hf);
|
| | | if(line=="" && FileIsEnding(hf)) break;
|
| | | if(StringFind(line, "by_symbol_strategy_timeframe", 0)>=0) { in_section=true; continue; }
|
| | | if(in_section)
|
| | | {
|
| | | if(StringFind(line, "]", 0)>=0) { in_section=false; continue; }
|
| | | if(StringFind(line, "\"strategy\"", 0)>=0)
|
| | | {
|
| | | // count as one slice; best-effort unique extraction
|
| | | slice_count++;
|
| | | // strategy
|
| | | int ks = StringFind(line, "\"strategy\"", 0);
|
| | | if(ks>=0){ int cs=StringFind(line, ":", ks); int q1=StringFind(line, "\"", cs); int q2=StringFind(line, "\"", q1+1); if(q1>=0 && q2>q1) PushUniqueS(uniq_strats, StringSubstr(line, q1+1, q2-q1-1)); }
|
| | | // symbol
|
| | | int ky = StringFind(line, "\"symbol\"", 0);
|
| | | if(ky>=0){ int cs2=StringFind(line, ":", ky); int q1=StringFind(line, "\"", cs2); int q2=StringFind(line, "\"", q1+1); if(q1>=0 && q2>q1) PushUniqueS(uniq_syms, StringSubstr(line, q1+1, q2-q1-1)); }
|
| | | // timeframe
|
| | | int kt = StringFind(line, "timeframe", 0);
|
| | | if(kt>=0){ int ct=StringFind(line, ":", kt); if(ct>=0){ string num=TrimCopy(StringSubstr(line, ct+1)); int tf=(int)StringToInteger(num); PushUniqueI(uniq_tfs, tf); } }
|
| | | }
|
| | | }
|
| | | }
|
| | | FileClose(hf);
|
| | | }
|
| | | }
|
| | |
|
| | | // Optional expected coverage
|
| | | string reqSyms[]; string reqStrats[]; string reqTFsS[]; SplitCSVList(RequireSymbolsCSV, reqSyms); SplitCSVList(RequireStrategiesCSV, reqStrats); SplitCSVList(RequireTimeframesCSV, reqTFsS);
|
| | | int reqTFs[]; for(int i=0;i<ArraySize(reqTFsS);++i) { int v=(int)StringToInteger(reqTFsS[i]); PushUniqueI(reqTFs, v);}
|
| | |
|
| | | // Merge requirements from optional Common Files config
|
| | | string cfgSymsCSV="", cfgTFsCSV="", cfgStratsCSV=""; bool cfgFail=false;
|
| | | LoadRequirementsFromConfig(RequirementsConfig, cfgSymsCSV, cfgTFsCSV, cfgStratsCSV, cfgFail);
|
| | | string cfgSyms[]; string cfgStrats[]; string cfgTFsS[];
|
| | | SplitCSVList(cfgSymsCSV, cfgSyms); SplitCSVList(cfgStratsCSV, cfgStrats); SplitCSVList(cfgTFsCSV, cfgTFsS);
|
| | | int cfgTFs[]; for(int i=0;i<ArraySize(cfgTFsS);++i){ int v=(int)StringToInteger(cfgTFsS[i]); PushUniqueI(cfgTFs, v);}
|
| | | for(int i=0;i<ArraySize(cfgSyms);++i) PushUniqueS(reqSyms, cfgSyms[i]);
|
| | | for(int i=0;i<ArraySize(cfgStrats);++i) PushUniqueS(reqStrats, cfgStrats[i]);
|
| | | for(int i=0;i<ArraySize(cfgTFs);++i) PushUniqueI(reqTFs, cfgTFs[i]);
|
| | | bool fail_missing_required = (FailOnMissingRequired || cfgFail);
|
| | |
|
| | | // Build report
|
| | | string report="";
|
| | | string common_path = TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files\\DualEA";
|
| | | report += "parser_version: 2\n";
|
| | | report += "common_path: "+common_path+"\n";
|
| | | report += "insights.json mtime : "+TimeToString(t_ins, TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"\n";
|
| | | if(feat_exists) report += "features.csv mtime : "+TimeToString(t_feat, TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"\n";
|
| | | if(kb_exists) report += "knowledge_base.csv mtime : "+TimeToString(t_kb, TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"\n";
|
2025-09-12 20:46:11 -04:00 | | | report += "stale: "+(is_stale?"true":"false")+"\n";
|
| | | report += "totals.trade_count: "+IntegerToString(total_trade_count)+"\n";
|
| | | report += "by_strategy.count: "+IntegerToString(cnt_by_strategy)+"\n";
|
| | | report += "by_timeframe.count: "+IntegerToString(cnt_by_tf)+"\n";
|
| | | report += "by_symbol_strategy_timeframe.count: "+IntegerToString(slice_count)+"\n";
|
| | | report += "uniques: symbols="+IntegerToString(ArraySize(uniq_syms))+" strategies="+IntegerToString(ArraySize(uniq_strats))+" timeframes="+IntegerToString(ArraySize(uniq_tfs))+"\n";
|
2025-08-13 19:41:57 -04:00 | | | report += "uniques.symbols: "+JoinStrings(uniq_syms, ",")+"\n";
|
| | | report += "uniques.strategies: "+JoinStrings(uniq_strats, ",")+"\n";
|
| | | report += "uniques.timeframes: "+JoinInts(uniq_tfs, ",")+"\n";
|
| | |
|
| | | // Alerts
|
| | | bool has_errors=false;
|
| | | if(is_stale) { report += "ALERT: insights.json is STALE vs sources\n"; if(FailOnStale) has_errors=true; }
|
| | | if(slice_count==0) { report += "ALERT: no slices in by_symbol_strategy_timeframe\n"; if(FailOnEmptySlices) has_errors=true; }
|
| | |
|
| | | bool missing_required=false;
|
| | | for(int i=0;i<ArraySize(reqSyms);++i)
|
| | | {
|
| | | bool present=false; for(int j=0;j<ArraySize(uniq_syms);++j) if(uniq_syms[j]==reqSyms[i]) { present=true; break; }
|
2025-09-12 20:46:11 -04:00 | | | if(!present) { report += "WARN: required symbol missing: "+reqSyms[i]+"\n"; missing_required=true; }
|
2025-08-13 19:41:57 -04:00 | | | }
|
| | | for(int i=0;i<ArraySize(reqStrats);++i)
|
| | | {
|
| | | bool present=false; for(int j=0;j<ArraySize(uniq_strats);++j) if(uniq_strats[j]==reqStrats[i]) { present=true; break; }
|
2025-09-12 20:46:11 -04:00 | | | if(!present) { report += "WARN: required strategy missing: "+reqStrats[i]+"\n"; missing_required=true; }
|
2025-08-13 19:41:57 -04:00 | | | }
|
| | | for(int i=0;i<ArraySize(reqTFs);++i)
|
| | | {
|
| | | bool present=false; for(int j=0;j<ArraySize(uniq_tfs);++j) if(uniq_tfs[j]==reqTFs[i]) { present=true; break; }
|
2025-09-12 20:46:11 -04:00 | | | if(!present) { report += "WARN: required timeframe missing: "+IntegerToString(reqTFs[i])+"\n"; missing_required=true; }
|
2025-08-13 19:41:57 -04:00 | | | }
|
| | | if(missing_required && fail_missing_required) { report += "ALERT: required coverage missing\n"; has_errors=true; }
|
| | |
|
| | | if(WriteReport)
|
| | | {
|
| | | int w = FileOpen(ReportFile, FILE_WRITE|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
| | | if(w!=INVALID_HANDLE) { FileWriteString(w, report); FileClose(w); }
|
| | | else { PrintFormat("[VALIDATE] cannot write report '%s' (err=%d)", ReportFile, GetLastError()); }
|
| | | }
|
| | |
|
| | | if(has_errors) Print("[VALIDATE] FAILED"); else Print("[VALIDATE] OK");
|
| | | Print("[VALIDATE] Summary:\n"+report);
|
| | | }
|
| | |
|
| | | void OnStart()
|
| | | {
|
| | | Print("ValidateInsights: starting");
|
| | | Validate();
|
| | | Print("ValidateInsights: done");
|
| | | }
|