409 satır
15 KiloByte
MQL5
409 satır
15 KiloByte
MQL5
//+------------------------------------------------------------------+
| |||
//| 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();
| |||
}
|