ictCore/Experts/core/signalLab.mqh

409 satır
15 KiloByte
MQL5
HamKalıcı BağlantıNormal GörünümGeçmiş

2026-06-09 22:48:38 +03:00
//+------------------------------------------------------------------+
//| 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();
}