#ifndef __MODEL_PREDICTOR_MQH__ #define __MODEL_PREDICTOR_MQH__ // CRITICAL: DLL imports crash Strategy Tester but are needed for live trading // Use conditional compilation - only import DLL when NOT in tester #ifndef MQL_TESTER #ifdef USE_MOCK_ORT #import "PaperEA_OnnxBridge_mock.dll" int InitializeModel(string modelPath, string configPath); int PredictSignal(const double &features[], int featureCount, double &probability); void Cleanup(); string GetLastModelError(); #import #else #import "PaperEA_OnnxBridge_u2.dll" int InitializeModel(string modelPath, string configPath); int PredictSignal(const double &features[], int featureCount, double &probability); void Cleanup(); string GetLastModelError(); #import #endif #else // Strategy Tester: Use stub implementations int InitializeModel(string modelPath, string configPath) { return 0; } int PredictSignal(const double &features[], int featureCount, double &probability) { probability = 0.5; return 0; } void Cleanup() { } string GetLastModelError() { return "ONNX DLL not available in Strategy Tester"; } #endif class CModelPredictor { private: struct SCategoricalMap { string field; string keys[]; int values[]; }; bool m_ready; string m_model_path; string m_json_path; string m_ini_path; string m_input_name; string m_output_name; string m_last_error; double m_scaler_mean[]; double m_scaler_scale[]; string m_feature_names[]; double m_work_buffer[]; double m_last_probability; SCategoricalMap m_cat_maps[]; bool m_debug_logged; bool m_debug_probs_logged; bool LoadIniConfig(const string path); bool ParseKeyValue(const string line, string &key, string &value) const; static string Trim(string value); static string NormalizeKey(string value); void EnsureBuffer(const int count); void Log(const string msg) const { Print("[ModelPredictor] " + msg); } void LogError(const string msg); void CaptureDllError(const string context); public: CModelPredictor(); bool Init(const string model_path, const string config_json_path, const string config_ini_path); double Predict(double &features[], int feature_count); void Shutdown(); bool IsReady() const { return m_ready; } double LastProbability() const { return m_last_probability; } string LastError() const { return m_last_error; } int FeatureCount() const { return ArraySize(m_feature_names); } bool GetFeatureNames(string &dest[]) const; int EncodeCategorical(const string field, const string value) const; }; //+------------------------------------------------------------------+ //| Implementation | //+------------------------------------------------------------------+ CModelPredictor::CModelPredictor() { m_ready = false; m_model_path = ""; m_json_path = ""; m_ini_path = ""; m_input_name = ""; m_output_name = ""; m_last_error = ""; m_last_probability = 0.5; m_debug_logged = false; m_debug_probs_logged = false; ArrayResize(m_scaler_mean, 0); ArrayResize(m_scaler_scale, 0); ArrayResize(m_feature_names, 0); ArrayResize(m_work_buffer, 0); ArrayResize(m_cat_maps, 0); } string CModelPredictor::Trim(string value) { StringTrimLeft(value); StringTrimRight(value); return value; } string CModelPredictor::NormalizeKey(string value) { string tmp = Trim(value); while(StringLen(tmp) > 0) { int ch = StringGetCharacter(tmp, 0); if(ch == 0xFEFF || ch == 65279 || ch == 0) tmp = StringSubstr(tmp, 1); else break; } StringToLower(tmp); return tmp; } bool CModelPredictor::ParseKeyValue(const string line, string &key, string &value) const { int pos = StringFind(line, "="); if(pos <= 0) return false; key = Trim(StringSubstr(line, 0, pos)); value = Trim(StringSubstr(line, pos + 1)); key = NormalizeKey(key); return (StringLen(key) > 0); } void CModelPredictor::EnsureBuffer(const int count) { if(ArraySize(m_work_buffer) != count) ArrayResize(m_work_buffer, count); } void CModelPredictor::LogError(const string msg) { m_last_error = msg; Log("ERROR: " + msg); } void CModelPredictor::CaptureDllError(const string context) { string dll_err = GetLastModelError(); if(StringLen(Trim(dll_err)) == 0) dll_err = StringFormat("%s failed (GetLastError=%d)", context, GetLastError()); LogError(dll_err); } bool CModelPredictor::LoadIniConfig(const string path) { string open_name = path; int extra_flags = 0; string common_prefix = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\"; if(StringFind(path, common_prefix) == 0) { open_name = StringSubstr(path, StringLen(common_prefix)); extra_flags = FILE_COMMON; } // Try different file opening modes int handle = FileOpen(open_name, FILE_READ|FILE_TXT|extra_flags); if(handle == INVALID_HANDLE) { handle = FileOpen(open_name, FILE_READ|FILE_TXT|FILE_ANSI|extra_flags); } if(handle == INVALID_HANDLE) { handle = FileOpen(open_name, FILE_READ|FILE_TXT|FILE_UNICODE|extra_flags); } if(handle == INVALID_HANDLE) { handle = FileOpen(open_name, FILE_READ|FILE_TXT|FILE_COMMON); } if(handle == INVALID_HANDLE) { LogError(StringFormat("Unable to open ONNX config INI: %s (error=%d)", path, GetLastError())); return false; } int parsed_pairs = 0; bool saw_feature_names = false; ArrayResize(m_feature_names, 0); ArrayResize(m_scaler_mean, 0); ArrayResize(m_scaler_scale, 0); ArrayResize(m_cat_maps, 0); int declared_count = -1; while(!FileIsEnding(handle)) { string raw = Trim(FileReadString(handle)); if(StringLen(raw) == 0 || StringGetCharacter(raw, 0) == '#') continue; string key, value; if(!ParseKeyValue(raw, key, value)) continue; parsed_pairs++; if(parsed_pairs <= 3) Log(StringFormat("INI key[%d]=%s", parsed_pairs, key)); if(key == "feature_count") { declared_count = (int)StringToInteger(value); continue; } if(key == "feature_names") { string tokens[]; int parts = StringSplit(value, '|', tokens); if(parts <= 1) parts = StringSplit(value, ',', tokens); ArrayResize(m_feature_names, parts); for(int i = 0; i < parts; i++) m_feature_names[i] = Trim(tokens[i]); saw_feature_names = (parts > 0); Log(StringFormat("INI loaded feature_names=%d", parts)); continue; } if(key == "scaler_mean") { string tokens[]; int parts = StringSplit(value, '|', tokens); if(parts <= 1) parts = StringSplit(value, ',', tokens); ArrayResize(m_scaler_mean, parts); for(int i = 0; i < parts; i++) m_scaler_mean[i] = StringToDouble(Trim(tokens[i])); Log(StringFormat("INI loaded scaler_mean=%d", parts)); continue; } if(key == "scaler_scale") { string tokens[]; int parts = StringSplit(value, '|', tokens); if(parts <= 1) parts = StringSplit(value, ',', tokens); ArrayResize(m_scaler_scale, parts); for(int i = 0; i < parts; i++) m_scaler_scale[i] = StringToDouble(Trim(tokens[i])); Log(StringFormat("INI loaded scaler_scale=%d", parts)); continue; } if(StringSubstr(key, 0, 4) == "cat_") { string field = NormalizeKey(StringSubstr(key, 4)); string entries[]; int entry_count = StringSplit(value, '|', entries); if(entry_count <= 0) continue; SCategoricalMap map; map.field = field; ArrayResize(map.keys, 0); ArrayResize(map.values, 0); for(int e = 0; e < entry_count; e++) { string kv = Trim(entries[e]); int colon = StringFind(kv, ":"); if(colon <= 0) continue; string label = NormalizeKey(StringSubstr(kv, 0, colon)); int code = (int)StringToInteger(StringSubstr(kv, colon + 1)); int idx = ArraySize(map.keys); ArrayResize(map.keys, idx + 1); ArrayResize(map.values, idx + 1); map.keys[idx] = label; map.values[idx] = code; } int mcount = ArraySize(map.keys); if(mcount > 0) { int slot = ArraySize(m_cat_maps); ArrayResize(m_cat_maps, slot + 1); m_cat_maps[slot] = map; } continue; } if(key == "input_name") { m_input_name = Trim(value); continue; } if(key == "output_name") { m_output_name = Trim(value); continue; } } if(parsed_pairs == 0) { FileClose(handle); int hbin = FileOpen(open_name, FILE_READ|FILE_BIN|extra_flags); if(hbin == INVALID_HANDLE) hbin = FileOpen(open_name, FILE_READ|FILE_BIN|FILE_COMMON); if(hbin != INVALID_HANDLE) { int sz = (int)FileSize(hbin); if(sz > 0) { uchar bytes[]; ArrayResize(bytes, sz); FileReadArray(hbin, bytes, 0, sz); string content = CharArrayToString(bytes, 0, sz, 65001); if(StringLen(content) == 0) content = CharArrayToString(bytes, 0, sz, 0); string lines[]; int lcount = StringSplit(content, '\n', lines); for(int li = 0; li < lcount; li++) { string line = Trim(lines[li]); if(StringLen(line) == 0) continue; if(StringGetCharacter(line, StringLen(line) - 1) == '\r') line = StringSubstr(line, 0, StringLen(line) - 1); if(StringLen(line) == 0 || StringGetCharacter(line, 0) == '#') continue; string k2, v2; if(!ParseKeyValue(line, k2, v2)) continue; parsed_pairs++; if(k2 == "feature_count") { declared_count = (int)StringToInteger(v2); continue; } if(k2 == "feature_names") { string tokens[]; int parts = StringSplit(v2, '|', tokens); if(parts <= 1) parts = StringSplit(v2, ',', tokens); ArrayResize(m_feature_names, parts); for(int i = 0; i < parts; i++) m_feature_names[i] = Trim(tokens[i]); saw_feature_names = (parts > 0); continue; } if(k2 == "scaler_mean") { string tokens[]; int parts = StringSplit(v2, '|', tokens); if(parts <= 1) parts = StringSplit(v2, ',', tokens); ArrayResize(m_scaler_mean, parts); for(int i = 0; i < parts; i++) m_scaler_mean[i] = StringToDouble(Trim(tokens[i])); continue; } if(k2 == "scaler_scale") { string tokens[]; int parts = StringSplit(v2, '|', tokens); if(parts <= 1) parts = StringSplit(v2, ',', tokens); ArrayResize(m_scaler_scale, parts); for(int i = 0; i < parts; i++) m_scaler_scale[i] = StringToDouble(Trim(tokens[i])); continue; } if(StringSubstr(k2, 0, 4) == "cat_") { string field = NormalizeKey(StringSubstr(k2, 4)); string entries[]; int entry_count = StringSplit(v2, '|', entries); if(entry_count <= 0) continue; SCategoricalMap map; map.field = field; ArrayResize(map.keys, 0); ArrayResize(map.values, 0); for(int e = 0; e < entry_count; e++) { string kv = Trim(entries[e]); int colon = StringFind(kv, ":"); if(colon <= 0) continue; string label = NormalizeKey(StringSubstr(kv, 0, colon)); int code = (int)StringToInteger(StringSubstr(kv, colon + 1)); int idx = ArraySize(map.keys); ArrayResize(map.keys, idx + 1); ArrayResize(map.values, idx + 1); map.keys[idx] = label; map.values[idx] = code; } int mcount = ArraySize(map.keys); if(mcount > 0) { int slot = ArraySize(m_cat_maps); ArrayResize(m_cat_maps, slot + 1); m_cat_maps[slot] = map; } continue; } if(k2 == "input_name") { m_input_name = Trim(v2); continue; } if(k2 == "output_name") { m_output_name = Trim(v2); continue; } } } FileClose(hbin); } } else { FileClose(handle); } if(ArraySize(m_feature_names) == 0) { if(parsed_pairs == 0) LogError("ONNX config INI parsed 0 key/value pairs"); else if(!saw_feature_names) LogError(StringFormat("ONNX config parsed %d keys but did not detect feature_names", parsed_pairs)); LogError("ONNX config missing feature_names entry"); return false; } if(declared_count > 0 && declared_count != ArraySize(m_feature_names)) { Log(StringFormat("feature_count (%d) differs from actual list (%d) - continuing", declared_count, ArraySize(m_feature_names))); } if(ArraySize(m_scaler_mean) != ArraySize(m_feature_names)) { Log("Scaler mean length mismatch - padding"); ArrayResize(m_scaler_mean, ArraySize(m_feature_names)); } if(ArraySize(m_scaler_scale) != ArraySize(m_feature_names)) { Log("Scaler scale length mismatch - padding"); ArrayResize(m_scaler_scale, ArraySize(m_feature_names)); } return true; } bool CModelPredictor::Init(const string model_path, const string config_json_path, const string config_ini_path) { Shutdown(); if(StringLen(Trim(model_path)) == 0 || StringLen(Trim(config_json_path)) == 0 || StringLen(Trim(config_ini_path)) == 0) { LogError("Init parameters missing model/config paths"); return false; } if(!LoadIniConfig(config_ini_path)) return false; // The DLL expects the INI config path (scaler + feature info), not the JSON. if(InitializeModel(model_path, config_ini_path) != 1) { CaptureDllError("InitializeModel"); return false; } m_model_path = model_path; m_json_path = config_json_path; m_ini_path = config_ini_path; m_last_probability = 0.5; m_last_error = ""; m_ready = true; Log(StringFormat("ONNX predictor initialized (%d features)", ArraySize(m_feature_names))); return true; } void CModelPredictor::Shutdown() { if(m_ready) { Cleanup(); m_ready = false; Log("ONNX predictor shutdown"); } } bool CModelPredictor::GetFeatureNames(string &dest[]) const { int count = ArraySize(m_feature_names); ArrayResize(dest, count); for(int i = 0; i < count; i++) dest[i] = m_feature_names[i]; return (count > 0); } int CModelPredictor::EncodeCategorical(const string field, const string value) const { string needle = NormalizeKey(field); string normalized_value = NormalizeKey(value); for(int i = 0; i < ArraySize(m_cat_maps); i++) { if(m_cat_maps[i].field != needle) continue; for(int j = 0; j < ArraySize(m_cat_maps[i].keys); j++) { if(m_cat_maps[i].keys[j] == normalized_value) return m_cat_maps[i].values[j]; } break; } return 0; } double CModelPredictor::Predict(double &features[], int feature_count) { if(!m_ready) { Log("ERROR: Predict() called but model not ready (m_ready=false)"); return 0.5; } int expected = ArraySize(m_feature_names); if(feature_count != expected) { LogError(StringFormat("Feature mismatch: got %d expected %d", feature_count, expected)); return 0.5; } // DEBUG: Always log first 8 features to diagnose zero-input issue string feat_str = "ONNX Input features: "; bool has_nonzero = false; for(int i = 0; i < MathMin(8, expected); i++) { feat_str += StringFormat("[%d]=%.6f ", i, features[i]); if(MathAbs(features[i]) > 0.0001) has_nonzero = true; } Log(feat_str); if(!has_nonzero) { Log("WARNING: All features are zero or near-zero - model will return neutral"); } double output_value = 0.5; // Call DLL prediction int result = PredictSignal(features, expected, output_value); if(result != 1) { CaptureDllError("PredictSignal"); LogError(StringFormat("PredictSignal FAILED with code %d, output=%.6f", result, output_value)); return 0.5; } // Check for neutral output if(MathAbs(output_value - 0.5) < 0.001) { Log(StringFormat("WARNING: Model returned neutral (%.6f) - possible model file corruption or zero input", output_value)); } else { Log(StringFormat("PredictSignal SUCCESS: output=%.6f", output_value)); } // Check GetLastModelError for any debug info from DLL string dbg = GetLastModelError(); if(StringLen(dbg) > 0 && dbg != "ONNX DLL not available in Strategy Tester") { Log(StringFormat("DLL status: %s", dbg)); } output_value = MathMax(0.0, MathMin(1.0, output_value)); m_last_probability = output_value; return output_value; } #endif