forked from animatedread/Warrior_EA
283 lines
13 KiB
MQL5
283 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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
|