//+------------------------------------------------------------------+ //| signalLab.mqh | //| Copyright 2026, Magnum Tech. | //| | //| A general-purpose R&D tool for testing ANY hypothesis — trading | //| signals, session behaviour, time-of-day patterns, etc. | //| | //| TWO classes are provided: | //| | //| ① CSignalLab — atomic confusion-matrix for ONE hypothesis | //| | //| ② CLabGroup — manages up to LAB_MAX_SLOTS named hypotheses | //| in a single object. Each slot is an independent| //| CSignalLab. This is the recommended entry point| //| when you have several hypotheses in one EA/script| //| | //| ──────────────────────────────────────────────────────────────── | //| QUICK-START | //| | //| #include "../core/signalLab.mqh" | //| CLabGroup lab; | //| | //| // Bool outcome — did a second condition follow? | //| lab.record("asian_consolidation", | //| isAsianSession(), // hypothesis | //| (high-low) < atr*0.5); // observation | //| | //| // Threshold outcome — did a continuous value beat a cut-off? | //| lab.record("queenpin", | //| queenpin(flag), // hypothesis | //| pipsMoved, 20.0, // observed value + cut | //| LAB_CMP_GTE); // ≥ 20 pips = success | //| | //| void OnDeinit(const int r) { lab.reportAll(); } | //| | //+------------------------------------------------------------------+ #property strict //--- maximum number of independent hypotheses tracked by CLabGroup #define LAB_MAX_SLOTS 32 //+------------------------------------------------------------------+ //| ENUM_LAB_COMPARE | //| Describes how a continuous observed value is compared against | //| a threshold to decide whether an outcome was "true". | //+------------------------------------------------------------------+ enum ENUM_LAB_COMPARE { LAB_CMP_GTE = 0, // observed >= threshold (e.g. pips gained >= 20) LAB_CMP_LTE = 1, // observed <= threshold (e.g. range <= 0.5*ATR) LAB_CMP_GT = 2, // observed > threshold LAB_CMP_LT = 3, // observed < threshold LAB_CMP_BTWN = 4 // loThreshold <= observed <= hiThreshold }; //+------------------------------------------------------------------+ //| CSignalLab | //| | //| Atomic confusion-matrix for a single hypothesis. | //| Language is intentionally generic: "hypothesis" = what you | //| predicted; "observation" = what actually happened. | //| | //| Confusion matrix layout: | //| TP hypothesis=true & observation=true → correct prediction | //| FP hypothesis=true & observation=false → false alarm | //| TN hypothesis=false & observation=false → correct silence | //| FN hypothesis=false & observation=true → missed it | //+------------------------------------------------------------------+ class CSignalLab { private: //--- confusion-matrix counters long m_tp; long m_fp; long m_tn; long m_fn; //--- occurrence counters long m_hypoCount; // times hypothesis was true long m_totalObs; // total observations recorded //--- identity string m_label; public: CSignalLab(); void setLabel(string lbl) { m_label = lbl; } string getLabel() const { return m_label; } //--- core record — bool outcome // hypothesis : was your prediction true this observation? // observation: was the real outcome true? void record(bool hypothesis, bool observation); //--- record with a continuous observed value compared to a threshold // observedValue : the measured quantity (pips, range, ATR%, etc.) // threshold : the cut-off for a "true" outcome // cmp : the comparison operator (default ≥) // hiThreshold : second bound, only used with LAB_CMP_BTWN void record(bool hypothesis, double observedValue, double threshold, ENUM_LAB_COMPARE cmp = LAB_CMP_GTE, double hiThreshold = 0.0); //--- getters long getTP() const { return m_tp; } long getFP() const { return m_fp; } long getTN() const { return m_tn; } long getFN() const { return m_fn; } long getHypoCount() const { return m_hypoCount; } long getTotalObs() const { return m_totalObs; } //--- derived metrics (return -1.0 when denominator is zero) double precision() const; // TP / (TP+FP) double recall() const; // TP / (TP+FN) double f1Score() const; double accuracy() const; // (TP+TN) / total double hypoRate() const; // hypothesis fires / total (%) double baseRate() const; // (TP+FN) / total — how often outcome is true regardless //--- output void report() const; //--- housekeeping void reset(); }; //-------------------------------------------------------------------- // CSignalLab implementation //-------------------------------------------------------------------- CSignalLab::CSignalLab() { m_tp = 0; m_fp = 0; m_tn = 0; m_fn = 0; m_hypoCount = 0; m_totalObs = 0; m_label = "Hypothesis"; } //--- internal helper: evaluate a continuous value against a threshold static bool _labEval(double val, double lo, double hi, ENUM_LAB_COMPARE cmp) { switch(cmp) { case LAB_CMP_GTE: return val >= lo; case LAB_CMP_LTE: return val <= lo; case LAB_CMP_GT: return val > lo; case LAB_CMP_LT: return val < lo; case LAB_CMP_BTWN: return (val >= lo && val <= hi); default: return false; } } void CSignalLab::record(bool hypothesis, bool observation) { m_totalObs++; if(hypothesis) { m_hypoCount++; if(observation) m_tp++; else m_fp++; } else { if(!observation) m_tn++; else m_fn++; } } void CSignalLab::record(bool hypothesis, double observedValue, double threshold, ENUM_LAB_COMPARE cmp, double hiThreshold) { bool observation = _labEval(observedValue, threshold, hiThreshold, cmp); record(hypothesis, observation); } double CSignalLab::precision() const { long d = m_tp + m_fp; return d == 0 ? -1.0 : (double)m_tp / d * 100.0; } double CSignalLab::recall() const { long d = m_tp + m_fn; return d == 0 ? -1.0 : (double)m_tp / d * 100.0; } double CSignalLab::f1Score() const { double p = precision(), r = recall(); if(p < 0 || r < 0) return -1.0; if(p + r == 0) return 0.0; return 2.0 * p * r / (p + r); } double CSignalLab::accuracy() const { long total = m_tp + m_fp + m_tn + m_fn; return total == 0 ? -1.0 : (double)(m_tp + m_tn) / total * 100.0; } double CSignalLab::hypoRate() const { return m_totalObs == 0 ? -1.0 : (double)m_hypoCount / m_totalObs * 100.0; } double CSignalLab::baseRate() const { // base rate = how often the outcome is true, hypothesis-independent long total = m_tp + m_fp + m_tn + m_fn; return total == 0 ? -1.0 : (double)(m_tp + m_fn) / total * 100.0; } void CSignalLab::report() const { string sep = "════════════════════════════════════════"; string sep2 = "────────────────────────────────────────"; PrintFormat("%s", sep); PrintFormat(" Hypothesis : \"%s\"", m_label); PrintFormat("%s", sep2); PrintFormat(" Total observations : %d", m_totalObs); PrintFormat(" Hypothesis fired : %d (%.2f%% of obs)", m_hypoCount, MathMax(hypoRate(), 0)); PrintFormat(" Base rate (outcome) : %.2f%%", MathMax(baseRate(), 0)); PrintFormat("%s", sep2); PrintFormat(" Confusion Matrix"); PrintFormat(" TP (correct prediction) : %d", m_tp); PrintFormat(" FP (false alarm) : %d", m_fp); PrintFormat(" TN (correct silence) : %d", m_tn); PrintFormat(" FN (missed) : %d", m_fn); PrintFormat("%s", sep2); PrintFormat(" Derived Metrics"); double p = precision(), r = recall(), f = f1Score(), a = accuracy(); PrintFormat(" Precision (hit rate when fired) : %s", p >= 0 ? StringFormat("%.2f%%", p) : "N/A"); PrintFormat(" Recall (coverage of outcomes): %s", r >= 0 ? StringFormat("%.2f%%", r) : "N/A"); PrintFormat(" F1 Score : %s", f >= 0 ? StringFormat("%.2f%%", f) : "N/A"); PrintFormat(" Accuracy (overall correctness) : %s", a >= 0 ? StringFormat("%.2f%%", a) : "N/A"); PrintFormat("%s", sep); } void CSignalLab::reset() { m_tp = m_fp = m_tn = m_fn = 0; m_hypoCount = 0; m_totalObs = 0; } //+------------------------------------------------------------------+ //| CLabGroup | //| | //| Manages up to LAB_MAX_SLOTS (32) independently named hypotheses | //| in one object. Slots are created on first use (lazy init). | //| | //| Usage: | //| CLabGroup lab; | //| | //| // Any string key — new slots are created automatically | //| lab.record("asian_session", isAsian(), rangeIsNarrow); | //| lab.record("queenpin_long", signal, pipsMoved, 20, LAB_CMP_GTE); | //| lab.record("monday_bullish", isMon(), close > open); | //| | //| lab.report("asian_session"); // single hypothesis | //| lab.reportAll(); // all registered hypotheses | //| lab.resetAll(); // zero everything | //+------------------------------------------------------------------+ class CLabGroup { private: CSignalLab m_slots[LAB_MAX_SLOTS]; string m_keys[LAB_MAX_SLOTS]; int m_count; int _findOrCreate(string key); public: CLabGroup(); //--- record — bool outcome bool record(string key, bool hypothesis, bool observation); //--- record — continuous threshold outcome bool record(string key, bool hypothesis, double observedValue, double threshold, ENUM_LAB_COMPARE cmp = LAB_CMP_GTE, double hiThreshold = 0.0); //--- access a named slot directly (for custom getters) CSignalLab *get(string key); //--- output void report(string key) const; void reportAll() const; //--- housekeeping void reset(string key); void resetAll(); int count() const { return m_count; } }; //-------------------------------------------------------------------- // CLabGroup implementation //-------------------------------------------------------------------- CLabGroup::CLabGroup() : m_count(0) { for(int i = 0; i < LAB_MAX_SLOTS; i++) m_keys[i] = ""; } int CLabGroup::_findOrCreate(string key) { // search existing slots for(int i = 0; i < m_count; i++) if(m_keys[i] == key) return i; // create a new slot if(m_count >= LAB_MAX_SLOTS) { PrintFormat("[CLabGroup] ERROR: slot limit (%d) reached — cannot add \"%s\"", LAB_MAX_SLOTS, key); return -1; } int idx = m_count++; m_keys[idx] = key; m_slots[idx].setLabel(key); return idx; } bool CLabGroup::record(string key, bool hypothesis, bool observation) { int idx = _findOrCreate(key); if(idx < 0) return false; m_slots[idx].record(hypothesis, observation); return true; } bool CLabGroup::record(string key, bool hypothesis, double observedValue, double threshold, ENUM_LAB_COMPARE cmp, double hiThreshold) { int idx = _findOrCreate(key); if(idx < 0) return false; m_slots[idx].record(hypothesis, observedValue, threshold, cmp, hiThreshold); return true; } CSignalLab *CLabGroup::get(string key) { for(int i = 0; i < m_count; i++) if(m_keys[i] == key) return &m_slots[i]; return NULL; } void CLabGroup::report(string key) const { for(int i = 0; i < m_count; i++) if(m_keys[i] == key) { m_slots[i].report(); return; } PrintFormat("[CLabGroup] No hypothesis named \"%s\" found.", key); } void CLabGroup::reportAll() const { if(m_count == 0) { Print("[CLabGroup] No hypotheses recorded yet."); return; } PrintFormat("╔══ CLabGroup — %d hypothesis/es ══╗", m_count); for(int i = 0; i < m_count; i++) m_slots[i].report(); Print("╚══ End of Report ══╝"); } void CLabGroup::reset(string key) { for(int i = 0; i < m_count; i++) if(m_keys[i] == key) { m_slots[i].reset(); return; } } void CLabGroup::resetAll() { for(int i = 0; i < m_count; i++) m_slots[i].reset(); }