409 lines
15 KiB
MQL5
409 lines
15 KiB
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();
|
|
}
|