2026-01-14 16:17:00 -05:00 | | | #ifndef __MODEL_PREDICTOR_MQH__
|
| | | #define __MODEL_PREDICTOR_MQH__
|
| | |
|
2026-02-24 12:47:37 -05:00 | | | // 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
|
| | |
|
2026-02-04 14:28:59 -05:00 | | | #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();
|
2026-01-14 16:17:00 -05:00 | | | #import
|
2026-02-04 14:28:59 -05:00 | | | #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
|
2026-01-14 16:17:00 -05:00 | | |
|
2026-02-24 12:47:37 -05:00 | | | #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
|
| | |
|
2026-01-14 16:17:00 -05:00 | | | 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[];
|
2026-02-04 14:28:59 -05:00 | | | bool m_debug_logged;
|
| | | bool m_debug_probs_logged;
|
2026-01-14 16:17:00 -05:00 | | |
|
| | | 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;
|
2026-02-04 14:28:59 -05:00 | | | m_debug_logged = false;
|
| | | m_debug_probs_logged = false;
|
2026-01-14 16:17:00 -05:00 | | | 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);
|
2026-02-04 14:28:59 -05:00 | | | while(StringLen(tmp) > 0)
|
| | | {
|
| | | int ch = StringGetCharacter(tmp, 0);
|
| | | if(ch == 0xFEFF || ch == 65279 || ch == 0)
|
| | | tmp = StringSubstr(tmp, 1);
|
| | | else
|
| | | break;
|
| | | }
|
2026-01-14 16:17:00 -05:00 | | | 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)
|
| | | {
|
2026-02-04 14:28:59 -05:00 | | | string dll_err = GetLastModelError();
|
| | | if(StringLen(Trim(dll_err)) == 0)
|
2026-01-14 16:17:00 -05:00 | | | dll_err = StringFormat("%s failed (GetLastError=%d)", context, GetLastError());
|
| | | LogError(dll_err);
|
| | | }
|
| | |
|
2026-02-04 14:28:59 -05:00 | | | 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);
|
2026-01-14 16:17:00 -05:00 | | | if(handle == INVALID_HANDLE)
|
| | | {
|
2026-02-04 14:28:59 -05:00 | | | 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;
|
2026-01-14 16:17:00 -05:00 | | | }
|
| | |
|
2026-02-04 14:28:59 -05:00 | | | int parsed_pairs = 0;
|
| | | bool saw_feature_names = false;
|
| | |
|
2026-01-14 16:17:00 -05:00 | | | 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;
|
| | |
|
2026-02-04 14:28:59 -05:00 | | | parsed_pairs++;
|
| | | if(parsed_pairs <= 3)
|
| | | Log(StringFormat("INI key[%d]=%s", parsed_pairs, key));
|
| | |
|
2026-01-14 16:17:00 -05:00 | | | if(key == "feature_count")
|
| | | {
|
| | | declared_count = (int)StringToInteger(value);
|
| | | continue;
|
| | | }
|
| | |
|
| | | if(key == "feature_names")
|
| | | {
|
| | | string tokens[];
|
| | | int parts = StringSplit(value, '|', tokens);
|
2026-02-04 14:28:59 -05:00 | | | if(parts <= 1)
|
| | | parts = StringSplit(value, ',', tokens);
|
2026-01-14 16:17:00 -05:00 | | | ArrayResize(m_feature_names, parts);
|
| | | for(int i = 0; i < parts; i++)
|
| | | m_feature_names[i] = Trim(tokens[i]);
|
2026-02-04 14:28:59 -05:00 | | | saw_feature_names = (parts > 0);
|
| | | Log(StringFormat("INI loaded feature_names=%d", parts));
|
2026-01-14 16:17:00 -05:00 | | | continue;
|
| | | }
|
| | |
|
| | | if(key == "scaler_mean")
|
| | | {
|
| | | string tokens[];
|
| | | int parts = StringSplit(value, '|', tokens);
|
2026-02-04 14:28:59 -05:00 | | | if(parts <= 1)
|
| | | parts = StringSplit(value, ',', tokens);
|
2026-01-14 16:17:00 -05:00 | | | ArrayResize(m_scaler_mean, parts);
|
| | | for(int i = 0; i < parts; i++)
|
| | | m_scaler_mean[i] = StringToDouble(Trim(tokens[i]));
|
2026-02-04 14:28:59 -05:00 | | | Log(StringFormat("INI loaded scaler_mean=%d", parts));
|
2026-01-14 16:17:00 -05:00 | | | continue;
|
| | | }
|
| | |
|
| | | if(key == "scaler_scale")
|
| | | {
|
| | | string tokens[];
|
| | | int parts = StringSplit(value, '|', tokens);
|
2026-02-04 14:28:59 -05:00 | | | if(parts <= 1)
|
| | | parts = StringSplit(value, ',', tokens);
|
2026-01-14 16:17:00 -05:00 | | | ArrayResize(m_scaler_scale, parts);
|
| | | for(int i = 0; i < parts; i++)
|
| | | m_scaler_scale[i] = StringToDouble(Trim(tokens[i]));
|
2026-02-04 14:28:59 -05:00 | | | Log(StringFormat("INI loaded scaler_scale=%d", parts));
|
2026-01-14 16:17:00 -05:00 | | | 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;
|
| | | }
|
| | | }
|
| | |
|
2026-02-04 14:28:59 -05:00 | | | 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);
|
| | | }
|
2026-01-14 16:17:00 -05:00 | | |
|
| | | if(ArraySize(m_feature_names) == 0)
|
| | | {
|
2026-02-04 14:28:59 -05:00 | | | 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));
|
2026-01-14 16:17:00 -05:00 | | | 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;
|
| | |
|
2026-02-04 14:28:59 -05:00 | | | // The DLL expects the INI config path (scaler + feature info), not the JSON.
|
| | | if(InitializeModel(model_path, config_ini_path) != 1)
|
2026-01-14 16:17:00 -05:00 | | | {
|
2026-02-04 14:28:59 -05:00 | | | CaptureDllError("InitializeModel");
|
2026-01-14 16:17:00 -05:00 | | | 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)
|
| | | {
|
2026-02-04 14:28:59 -05:00 | | | Cleanup();
|
2026-01-14 16:17:00 -05:00 | | | 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)
|
2026-03-09 15:23:42 -04:00 | | | {
|
| | | Log("ERROR: Predict() called but model not ready (m_ready=false)");
|
2026-01-14 16:17:00 -05:00 | | | return 0.5;
|
2026-03-09 15:23:42 -04:00 | | | }
|
2026-01-14 16:17:00 -05:00 | | |
|
| | | int expected = ArraySize(m_feature_names);
|
| | | if(feature_count != expected)
|
| | | {
|
| | | LogError(StringFormat("Feature mismatch: got %d expected %d", feature_count, expected));
|
| | | return 0.5;
|
| | | }
|
| | |
|
2026-03-09 15:23:42 -04:00 | | | // 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++)
|
2026-01-14 16:17:00 -05:00 | | | {
|
2026-03-09 15:23:42 -04:00 | | | 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");
|
2026-01-14 16:17:00 -05:00 | | | }
|
| | |
|
2026-03-09 15:23:42 -04:00 | | | double output_value = 0.5;
|
| | |
|
| | | // Call DLL prediction
|
| | | int result = PredictSignal(features, expected, output_value);
|
| | |
|
| | | if(result != 1)
|
2026-01-14 16:17:00 -05:00 | | | {
|
2026-02-04 14:28:59 -05:00 | | | CaptureDllError("PredictSignal");
|
2026-03-09 15:23:42 -04:00 | | | LogError(StringFormat("PredictSignal FAILED with code %d, output=%.6f", result, output_value));
|
2026-01-14 16:17:00 -05:00 | | | return 0.5;
|
| | | }
|
| | |
|
2026-03-09 15:23:42 -04:00 | | | // Check for neutral output
|
| | | if(MathAbs(output_value - 0.5) < 0.001)
|
2026-02-04 14:28:59 -05:00 | | | {
|
2026-03-09 15:23:42 -04:00 | | | 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));
|
2026-02-04 14:28:59 -05:00 | | | }
|
| | |
|
2026-01-14 16:17:00 -05:00 | | | output_value = MathMax(0.0, MathMin(1.0, output_value));
|
| | | m_last_probability = output_value;
|
| | | return output_value;
|
| | | }
|
| | |
|
| | | #endif
|