| //+------------------------------------------------------------------+
|
| //| VolMetaLabel.mqh |
|
| //| 48-72h unsigned expansion label + the live meta-label gate |
|
| //+------------------------------------------------------------------+
|
| //| THE LABEL (training side) |
|
| //| |
|
| //| threshold = K * ATR(t0) |
|
| //| path = bars t0+1 .. deadline |
|
| //| expansion = max( maxHigh(path) - entry , entry - minLow(path) ) |
|
| //| y = 1 if expansion >= threshold else 0 |
|
| //| |
|
| //| UNSIGNED ON PURPOSE. The current head is a 3-class softmax read |
|
| //| by Argmax3(pBuy,pSell,pNeutral) with ties falling to Neutral - |
|
| //| that is the majority-class trap, and it is structural, not a |
|
| //| tuning problem: Neutral is both the modal class AND the tiebreak. |
|
| //| A 2-class expansion/chop head has no third bucket to collapse to. |
|
| //| |
|
| //| WHY THIS IS A SMALLER CHANGE THAN IT LOOKS: LegRideLabel() is |
|
| //| ALREADY a magnitude question ("is there >= LEG_LABEL_MIN_RIDE_ATR |
|
| //| left in this leg"). It merely signs the answer with the leg's |
|
| //| direction at the end. This label asks the same magnitude question |
|
| //| on a fixed clock and drops the sign. |
|
| //| |
|
| //| *** THE WEEKEND CLIP IS LOAD-BEARING *** |
|
| //| A Wednesday trigger + 72h lands on Saturday. With a mandatory |
|
| //| Friday flat, scoring that path labels a position the strategy is |
|
| //| FORBIDDEN to hold. So the path is clipped at the weekly deadline |
|
| //| and a row whose clipped span is under H_min is UNRESOLVED, NOT 0. |
|
| //| Labelling it 0 would invent a chop class that is really "ran out |
|
| //| of week" - and that class correlates with day-of-week, which the |
|
| //| network already sees through its sin/cos time features. It would |
|
| //| learn the calendar and score well doing it. |
|
| //| Measured on H4 with a Friday 16:00 NY flat: ~43% of triggers fall |
|
| //| to this clip. That is a sample-size fact to plan around, not a |
|
| //| bug to code around. |
|
| //| |
|
| //| OVERLAP IS NOT OPTIONAL EITHER. A 72h label on H4 spans 18 bars, |
|
| //| so 18 consecutive rows resolve against overlapping paths. Report |
|
| //| PathBars() into the EXISTING CLabelOverlap/EffectiveSampleSize |
|
| //| machinery in Labels.mqh. A raw N here overstates significance by |
|
| //| roughly sqrt(18) ~ 4.2x. |
|
| //+------------------------------------------------------------------+
|
| #ifndef VOLMETALABEL_MQH
|
| #define VOLMETALABEL_MQH
|
|
|
| #include "SwapWindow.mqh"
|
|
|
| enum ENUM_VOL_LABEL
|
| {
|
| VOL_UNRESOLVED = -1, // dropped: clipped, or bad data. NEVER a class.
|
| VOL_CHOP = 0,
|
| VOL_EXPANSION = 1
|
| };
|
|
|
| //+------------------------------------------------------------------+
|
| //| CVolExpansionLabel |
|
| //+------------------------------------------------------------------+
|
| class CVolExpansionLabel
|
| {
|
| private:
|
| string m_symbol;
|
| ENUM_TIMEFRAMES m_period;
|
| double m_kAtr;
|
| int m_hMinHours;
|
| int m_hMaxHours;
|
| ENUM_DAY_OF_WEEK m_flatDay;
|
| int m_flatMinNy;
|
| //--- diagnostics for the last call
|
| int m_pathBars;
|
| double m_expansionAtr;
|
| int m_barsToHit;
|
|
|
| public:
|
| CVolExpansionLabel(void) : m_kAtr(2.0), m_hMinHours(48), m_hMaxHours(72),
|
| m_flatDay(FRIDAY), m_flatMinNy(16 * 60),
|
| m_pathBars(0), m_expansionAtr(0.0), m_barsToHit(-1) { }
|
|
|
| void Init(const string symbol, const ENUM_TIMEFRAMES period,
|
| const double kAtr, const int hMinHours, const int hMaxHours,
|
| const ENUM_DAY_OF_WEEK flatDay, const int flatMinNy)
|
| {
|
| m_symbol = symbol;
|
| m_period = period;
|
| m_kAtr = kAtr;
|
| m_hMinHours = hMinHours;
|
| m_hMaxHours = hMaxHours;
|
| m_flatDay = flatDay;
|
| m_flatMinNy = flatMinNy;
|
| }
|
|
|
| int PathBars(void) const { return m_pathBars; }
|
| double ExpansionAtr(void) const { return m_expansionAtr; }
|
| int BarsToHit(void) const { return m_barsToHit; }
|
|
|
| //+---------------------------------------------------------------+
|
| //| The weekly flat instant (UTC) for the week containing `utc`. |
|
| //+---------------------------------------------------------------+
|
| datetime WeeklyDeadlineUtc(const datetime utc) const
|
| {
|
| datetime ny = SWNewYorkTime(utc);
|
| MqlDateTime n;
|
| TimeToStruct(ny, n);
|
| int daysAhead = ((int)m_flatDay - n.day_of_week + 7) % 7;
|
| int nowMin = n.hour * 60 + n.min;
|
| if(daysAhead == 0 && nowMin >= m_flatMinNy)
|
| daysAhead = 7; // this week's cutoff already passed
|
| datetime flatNy = ny + daysAhead * 86400;
|
| MqlDateTime f;
|
| TimeToStruct(flatNy, f);
|
| f.hour = m_flatMinNy / 60;
|
| f.min = m_flatMinNy % 60;
|
| f.sec = 0;
|
| flatNy = StructToTime(f);
|
| //--- back to UTC using the offset AT THAT INSTANT, not today's
|
| return flatNy - SWNewYorkUtcOffset(flatNy);
|
| }
|
|
|
| //+---------------------------------------------------------------+
|
| //| Label the bar at series index `shift` (0 = newest). |
|
| //| |
|
| //| NO LOOKAHEAD BY CONSTRUCTION: the path is bars shift-1 .. 1, |
|
| //| i.e. strictly NEWER closed bars, and the entry/ATR come from |
|
| //| `shift` itself. A path that has not completed inside loaded |
|
| //| history returns VOL_UNRESOLVED and must be revisited, never |
|
| //| cached - the same finality rule AdvanceSwingLabelState() uses. |
|
| //+---------------------------------------------------------------+
|
| ENUM_VOL_LABEL Label(const int shift, const double entry, const double atr)
|
| {
|
| m_pathBars = 0;
|
| m_expansionAtr = 0.0;
|
| m_barsToHit = -1;
|
|
|
| if(shift < 1 || !MathIsValidNumber(entry) || entry <= 0.0 || entry == EMPTY_VALUE)
|
| return VOL_UNRESOLVED;
|
| //--- == EMPTY_VALUE explicitly: a cold ATR reads as DBL_MAX, which would
|
| //--- make the threshold unreachable and label the whole sweep CHOP.
|
| if(!MathIsValidNumber(atr) || atr <= 0.0 || atr == EMPTY_VALUE)
|
| return VOL_UNRESOLVED;
|
|
|
| datetime t0[];
|
| if(CopyTime(m_symbol, m_period, shift, 1, t0) != 1)
|
| return VOL_UNRESOLVED;
|
|
|
| datetime hardStop = t0[0] + m_hMaxHours * 3600;
|
| datetime weekStop = WeeklyDeadlineUtc(t0[0]);
|
| datetime deadline = (weekStop < hardStop) ? weekStop : hardStop;
|
|
|
| //--- walk toward NEWER bars (decreasing index) until the deadline
|
| int endShift = -1;
|
| for(int e = shift - 1; e >= 0; e--)
|
| {
|
| datetime te[];
|
| if(CopyTime(m_symbol, m_period, e, 1, te) != 1)
|
| break;
|
| if(te[0] > deadline)
|
| break;
|
| endShift = e;
|
| }
|
| if(endShift < 0 || endShift >= shift)
|
| return VOL_UNRESOLVED; // no path yet - leading edge
|
|
|
| datetime tEnd[];
|
| if(CopyTime(m_symbol, m_period, endShift, 1, tEnd) != 1)
|
| return VOL_UNRESOLVED;
|
| //--- HARD DROP, never a CHOP: a path cut short by the flat rule is
|
| //--- absence of evidence, not evidence of chop.
|
| if((int)(tEnd[0] - t0[0]) < m_hMinHours * 3600)
|
| return VOL_UNRESOLVED;
|
|
|
| int count = shift - endShift; // bars shift-1 .. endShift
|
| double hi[], lo[];
|
| if(CopyHigh(m_symbol, m_period, endShift, count, hi) != count ||
|
| CopyLow(m_symbol, m_period, endShift, count, lo) != count)
|
| return VOL_UNRESOLVED;
|
|
|
| double thr = m_kAtr * atr;
|
| double maxUp = 0.0, maxDn = 0.0;
|
| int firstHit = -1;
|
| //--- Copy* is oldest-first, so walk it backwards to visit the path in
|
| //--- chronological order and capture WHEN the barrier was first touched.
|
| for(int i = count - 1; i >= 0; i--)
|
| {
|
| if(!MathIsValidNumber(hi[i]) || !MathIsValidNumber(lo[i]) ||
|
| hi[i] == EMPTY_VALUE || lo[i] == EMPTY_VALUE || hi[i] <= 0.0 || lo[i] <= 0.0)
|
| return VOL_UNRESOLVED;
|
| if(hi[i] - entry > maxUp)
|
| maxUp = hi[i] - entry;
|
| if(entry - lo[i] > maxDn)
|
| maxDn = entry - lo[i];
|
| if(firstHit < 0 && (maxUp >= thr || maxDn >= thr))
|
| firstHit = count - i;
|
| }
|
|
|
| double expansion = MathMax(maxUp, maxDn);
|
| m_pathBars = count;
|
| m_expansionAtr = expansion / atr;
|
| m_barsToHit = firstHit;
|
| return (expansion >= thr) ? VOL_EXPANSION : VOL_CHOP;
|
| }
|
| };
|
|
|
| //+------------------------------------------------------------------+
|
| //| CVolMetaGate - the LIVE filter. |
|
| //| |
|
| //| *** WHERE THIS MUST BE CALLED, AND WHERE IT MUST NOT *** |
|
| //| |
|
| //| It must NOT be wired as another voting signal module. Two reasons,|
|
| //| both already paid for in this codebase: |
|
| //| - CExpertSignal has NO per-side veto. A module returning 0 is |
|
| //| an abstention that the ensemble averages away, not a block. |
|
| //| - Returning EMPTY_VALUE to mean "no" silences the WHOLE |
|
| //| ensemble, not one side. |
|
| //| A meta-label is a VETO, so it belongs on the entry path as a |
|
| //| single gate - structurally the same place, and the same shape, as |
|
| //| the TCCanOpen() gate: one call, one bool, one logged reason. |
|
| //| |
|
| //| Hook: CExpertCustom::Processing(), immediately before the |
|
| //| CheckOpenLong()/CheckOpenShort() call (ExpertCustom.mqh ~528). |
|
| //+------------------------------------------------------------------+
|
| class CVolMetaGate
|
| {
|
| private:
|
| double m_minCertainty; // e.g. 0.70
|
| double m_overrideMultiple; // swap-drag override margin
|
| bool m_enabled;
|
| string m_lastReason;
|
|
|
| public:
|
| CVolMetaGate(void) : m_minCertainty(0.70), m_overrideMultiple(3.0),
|
| m_enabled(true), m_lastReason("") { }
|
|
|
| void Init(const double minCertainty, const double overrideMultiple,
|
| const bool enabled)
|
| {
|
| m_minCertainty = minCertainty;
|
| m_overrideMultiple = overrideMultiple;
|
| m_enabled = enabled;
|
| }
|
|
|
| string LastReason(void) const { return m_lastReason; }
|
|
|
| //+---------------------------------------------------------------+
|
| //| pExpansion : network P(expansion) for the next 48-72h, 0..1 |
|
| //| Returns true when the primary pattern may proceed. |
|
| //| |
|
| //| A pExpansion outside [0,1] means the head did not produce a |
|
| //| reading. That BLOCKS. An unavailable meta-label must never |
|
| //| degrade to "allow" - that is how a filter silently stops |
|
| //| filtering while the log still says it is on. |
|
| //+---------------------------------------------------------------+
|
| bool Allowed(const string symbol, const bool isLong, const double lots,
|
| const double pExpansion, const double expectedMoveAtr,
|
| const double atrPrice)
|
| {
|
| if(!m_enabled)
|
| {
|
| m_lastReason = "meta-gate disabled";
|
| return true;
|
| }
|
| if(!MathIsValidNumber(pExpansion) || pExpansion < 0.0 || pExpansion > 1.0)
|
| {
|
| m_lastReason = "no meta-label reading - blocked";
|
| return false;
|
| }
|
| if(pExpansion < m_minCertainty)
|
| {
|
| m_lastReason = StringFormat("chop forecast: P(expansion)=%.3f < %.2f",
|
| pExpansion, m_minCertainty);
|
| return false;
|
| }
|
| string why = "";
|
| if(SWBlockedByTripleSwap(symbol, isLong, lots, expectedMoveAtr, atrPrice,
|
| m_overrideMultiple, why))
|
| {
|
| m_lastReason = why;
|
| return false;
|
| }
|
| m_lastReason = StringFormat("allowed: P(expansion)=%.3f%s", pExpansion,
|
| (StringLen(why) > 0 ? " (" + why + ")" : ""));
|
| return true;
|
| }
|
| };
|
| #endif // VOLMETALABEL_MQH
|