mql5/Experts/AnotherTriBible/Chapter4.mqh

773 lines
21 KiB
MQL5

2026-03-15 02:46:38 +02:00
//+------------------------------------------------------------------+
//| C_Chapter4_SupplyDemand.mqh |
//+------------------------------------------------------------------+
#property strict
#property version "1.00"
#include "AbstractChapter.mqh"
//========================
// Inputs
//========================
input int ch4_zoneLookbackBars = 350;
input int ch4_baseLookbackBars = 8;
input int ch4_momentumAvgPeriod = 12;
input double ch4_momentumBodyFactor = 1.20;
input int ch4_protectedTolerancePts = 35;
input int ch4_minZoneSeparationBars = 3;
input int ch4_refineMode = 1; // 0=full, 1=body, 2=half
input int ch4_minQualityScore = 3; // BOS + momentum + liquidity + imbalance
input bool ch4_requireBos = true;
input bool ch4_requireMomentum = true;
input bool ch4_requireLiquidity = false;
input bool ch4_requireImbalance = false;
input bool ch4_hideInvalidZones = false;
//========================
// Enums
//========================
enum EZoneSide { ZONE_DEMAND = 0, ZONE_SUPPLY = 1 };
enum EZoneType { ZONE_OB = 0, ZONE_EXTREME = 1, ZONE_DECISIONAL = 2 };
enum EZoneState { ZONE_FRESH = 0, ZONE_TOUCHED = 1, ZONE_MITIGATED = 2, ZONE_INVALID = 3 };
struct SupplyDemandZone {
int id;
EZoneSide side;
EZoneType type;
EZoneState state;
int leftBar;
int baseBar;
int breakBar;
int rightBar;
double fullTop;
double fullBottom;
double refinedTop;
double refinedBottom;
bool hasBos;
bool hasMomentum;
bool hasLiquidity;
bool hasImbalance;
bool isInducement;
int qualityScore;
int touchBar;
int mitigateBar;
int invalidateBar;
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
class C_Chapter4 : public CAbstractChapter {
private:
FairValue fairValue;
SupplyDemandZone zones[];
int nextZoneId;
int protectedHigh;
int protectedLow;
string K_ZONE(int id) { return "CH4_ZONE_" + (string)id; }
string K_ZONE_R(int id) { return "CH4_ZONE_R_" + (string)id; }
private:
//========================
// Core helpers
//========================
void PushZone(SupplyDemandZone &z) {
int n = ArraySize(zones);
ArrayResize(zones, n + 1);
zones[n] = z;
}
bool ZoneExists(EZoneSide side, int baseBar, int breakBar, double top, double bottom) {
for(int i = 0; i < ArraySize(zones); i++) {
if(zones[i].side != side)
continue;
if(zones[i].baseBar != baseBar)
continue;
if(zones[i].breakBar != breakBar)
continue;
if(MathAbs(zones[i].fullTop - top) <= _Point * 2 &&
MathAbs(zones[i].fullBottom - bottom) <= _Point * 2)
return true;
}
return false;
}
bool HasBullImbalanceAround(int breakBar) {
int from = breakBar - 1;
int to = breakBar + 1;
if(from < 1) from = 1;
if(to >= Bars()) to = Bars() - 1;
for(int i = from; i <= to; i++) {
Fvg fvg;
fairValue.getBullFvg(fvg, i);
if(fairValue.IsValidValue(fvg))
return true;
}
return false;
}
bool HasBearImbalanceAround(int breakBar) {
int from = breakBar - 1;
int to = breakBar + 1;
if(from < 1) from = 1;
if(to >= Bars()) to = Bars() - 1;
for(int i = from; i <= to; i++) {
Fvg fvg;
fairValue.getBearFvg(fvg, i);
if(fairValue.IsValidValue(fvg))
return true;
}
return false;
}
double AvgBodySize(int endIndex, int period) {
if(period <= 0)
return 0.0;
int from = endIndex - period + 1;
if(from < 0)
from = 0;
double sum = 0.0;
int count = 0;
for(int i = from; i <= endIndex; i++) {
if(!IsValidIndex(i))
continue;
sum += BodySize(i);
count++;
}
if(count == 0)
return 0.0;
return sum / count;
}
bool IsBullMomentumBar(int i) {
if(!IsBullishCandle(i))
return false;
double avg = AvgBodySize(i - 1, ch4_momentumAvgPeriod);
if(avg <= 0.0)
avg = BodySize(i);
return BodySize(i) >= avg * ch4_momentumBodyFactor;
}
bool IsBearMomentumBar(int i) {
if(!IsBearishCandle(i))
return false;
double avg = AvgBodySize(i - 1, ch4_momentumAvgPeriod);
if(avg <= 0.0)
avg = BodySize(i);
return BodySize(i) >= avg * ch4_momentumBodyFactor;
}
int FindBrokenSwingHigh(int breakBar) {
int start = breakBar - ch4_baseLookbackBars - 10;
if(start < 0) start = 0;
int last = -1;
for(int i = start; i < breakBar; i++) {
if(!IsSwingHigh(i))
continue;
if(BreakAboveHigh(i, breakBar))
last = i;
}
return last;
}
int FindBrokenSwingLow(int breakBar) {
int start = breakBar - ch4_baseLookbackBars - 10;
if(start < 0) start = 0;
int last = -1;
for(int i = start; i < breakBar; i++) {
if(!IsSwingLow(i))
continue;
if(BreakBelowLow(i, breakBar))
last = i;
}
return last;
}
int FindDemandBaseBar(int breakBar) {
int from = breakBar - ch4_baseLookbackBars;
if(from < 0) from = 0;
for(int i = breakBar - 1; i >= from; i--) {
if(IsBearishCandle(i) || IsSellToBuyCandle(i) || IsDojiCandle(i, 0.20))
return i;
}
return breakBar - 1;
}
int FindSupplyBaseBar(int breakBar) {
int from = breakBar - ch4_baseLookbackBars;
if(from < 0) from = 0;
for(int i = breakBar - 1; i >= from; i--) {
if(IsBullishCandle(i) || IsBuyToSellCandle(i) || IsDojiCandle(i, 0.20))
return i;
}
return breakBar - 1;
}
int FindBaseStartBar(int baseBar) {
if(!IsValidIndex(baseBar))
return -1;
int from = baseBar - ch4_baseLookbackBars;
if(from < 0) from = 0;
int left = baseBar;
for(int i = baseBar; i >= from; i--) {
if(RangeAt(i) <= 0.0)
continue;
left = i;
if(BodySize(i) > AvgBodySize(i, 3) * 1.4)
break;
}
return left;
}
void BuildDemandZone(int leftBar,int baseBar,int breakBar,double &fullTop,double &fullBottom,double &refinedTop,double &refinedBottom) {
fullTop = HighAt(baseBar);
fullBottom = LowAt(baseBar);
for(int i = leftBar; i <= baseBar; i++) {
if(HighAt(i) > fullTop) fullTop = HighAt(i);
if(LowAt(i) < fullBottom) fullBottom = LowAt(i);
}
if(ch4_refineMode == 0) {
refinedTop = fullTop;
refinedBottom = fullBottom;
} else if(ch4_refineMode == 1) {
refinedTop = BodyTop(baseBar);
refinedBottom = BodyBottom(baseBar);
} else {
refinedTop = fullTop;
refinedBottom = ZoneMid(fullTop, fullBottom);
}
}
void BuildSupplyZone(int leftBar,int baseBar,int breakBar,double &fullTop,double &fullBottom,double &refinedTop,double &refinedBottom) {
fullTop = HighAt(baseBar);
fullBottom = LowAt(baseBar);
for(int i = leftBar; i <= baseBar; i++) {
if(HighAt(i) > fullTop) fullTop = HighAt(i);
if(LowAt(i) < fullBottom) fullBottom = LowAt(i);
}
if(ch4_refineMode == 0) {
refinedTop = fullTop;
refinedBottom = fullBottom;
} else if(ch4_refineMode == 1) {
refinedTop = BodyTop(baseBar);
refinedBottom = BodyBottom(baseBar);
} else {
refinedTop = ZoneMid(fullTop, fullBottom);
refinedBottom = fullBottom;
}
}
bool NearProtectedLow(int baseBar) {
if(!IsValidIndex(protectedLow))
return false;
return NearPrice(LowAt(baseBar), LowAt(protectedLow), ch4_protectedTolerancePts);
}
bool NearProtectedHigh(int baseBar) {
if(!IsValidIndex(protectedHigh))
return false;
return NearPrice(HighAt(baseBar), HighAt(protectedHigh), ch4_protectedTolerancePts);
}
bool HasBullLiquidityContext(int baseBar, int breakBar) {
if(IsValidIndex(protectedLow)) {
int from = baseBar - 3;
if(from < 0) from = 0;
for(int i = from; i <= breakBar; i++) {
if(WickBelowLow(protectedLow, i) || BreakBelowLow(protectedLow, i))
return true;
}
}
int lowA = PrevSwingLowIndex(baseBar, 1);
int lowB = PrevSwingLowIndex(baseBar, 2);
if(IsValidIndex(lowA) && IsValidIndex(lowB)) {
if(NearPrice(LowAt(lowA), LowAt(lowB), ch4_protectedTolerancePts))
return true;
}
return false;
}
bool HasBearLiquidityContext(int baseBar, int breakBar) {
if(IsValidIndex(protectedHigh)) {
int from = baseBar - 3;
if(from < 0) from = 0;
for(int i = from; i <= breakBar; i++) {
if(WickAboveHigh(protectedHigh, i) || BreakAboveHigh(protectedHigh, i))
return true;
}
}
int highA = PrevSwingHighIndex(baseBar, 1);
int highB = PrevSwingHighIndex(baseBar, 2);
if(IsValidIndex(highA) && IsValidIndex(highB)) {
if(NearPrice(HighAt(highA), HighAt(highB), ch4_protectedTolerancePts))
return true;
}
return false;
}
EZoneType ClassifyDemandType(int baseBar, int breakBar) {
if(NearProtectedLow(baseBar))
return ZONE_EXTREME;
return ZONE_DECISIONAL;
}
EZoneType ClassifySupplyType(int baseBar, int breakBar) {
if(NearProtectedHigh(baseBar))
return ZONE_EXTREME;
return ZONE_DECISIONAL;
}
int ScoreZone(bool hasBos, bool hasMomentum, bool hasLiquidity, bool hasImbalance) {
int score = 0;
if(hasBos) score++;
if(hasMomentum) score++;
if(hasLiquidity) score++;
if(hasImbalance) score++;
return score;
}
bool PassesFilters(bool hasBos, bool hasMomentum, bool hasLiquidity, bool hasImbalance, int score) {
if(ch4_requireBos && !hasBos) return false;
if(ch4_requireMomentum && !hasMomentum) return false;
if(ch4_requireLiquidity && !hasLiquidity) return false;
if(ch4_requireImbalance && !hasImbalance) return false;
return score >= ch4_minQualityScore;
}
//========================
// Detection
//========================
void DetectDemandZones() {
int end = Bars() - 1;
if(end < 10)
return;
int start = end - ch4_zoneLookbackBars;
if(start < 5) start = 5;
for(int i = start; i <= end; i++) {
if(!IsBullMomentumBar(i))
continue;
int brokenHigh = FindBrokenSwingHigh(i);
bool hasBos = IsValidIndex(brokenHigh);
if(ch4_requireBos && !hasBos)
continue;
int baseBar = FindDemandBaseBar(i);
if(!IsValidIndex(baseBar))
continue;
int leftBar = FindBaseStartBar(baseBar);
if(!IsValidIndex(leftBar))
leftBar = baseBar;
double fullTop, fullBottom, refinedTop, refinedBottom;
BuildDemandZone(leftBar, baseBar, i, fullTop, fullBottom, refinedTop, refinedBottom);
bool hasMomentum = true;
bool hasLiquidity = HasBullLiquidityContext(baseBar, i);
bool hasImbalance = HasBullImbalanceAround(i);
int score = ScoreZone(hasBos, hasMomentum, hasLiquidity, hasImbalance);
if(!PassesFilters(hasBos, hasMomentum, hasLiquidity, hasImbalance, score))
continue;
if(ZoneExists(ZONE_DEMAND, baseBar, i, fullTop, fullBottom))
continue;
SupplyDemandZone z;
z.id = nextZoneId++;
z.side = ZONE_DEMAND;
z.type = ClassifyDemandType(baseBar, i);
z.state = ZONE_FRESH;
z.leftBar = leftBar;
z.baseBar = baseBar;
z.breakBar = i;
z.rightBar = i;
z.fullTop = fullTop;
z.fullBottom = fullBottom;
z.refinedTop = refinedTop;
z.refinedBottom = refinedBottom;
z.hasBos = hasBos;
z.hasMomentum = hasMomentum;
z.hasLiquidity = hasLiquidity;
z.hasImbalance = hasImbalance;
z.isInducement = false;
z.qualityScore = score;
z.touchBar = -1;
z.mitigateBar = -1;
z.invalidateBar = -1;
PushZone(z);
}
}
void DetectSupplyZones() {
int end = Bars() - 1;
if(end < 10)
return;
int start = end - ch4_zoneLookbackBars;
if(start < 5) start = 5;
for(int i = start; i <= end; i++) {
if(!IsBearMomentumBar(i))
continue;
int brokenLow = FindBrokenSwingLow(i);
bool hasBos = IsValidIndex(brokenLow);
if(ch4_requireBos && !hasBos)
continue;
int baseBar = FindSupplyBaseBar(i);
if(!IsValidIndex(baseBar))
continue;
int leftBar = FindBaseStartBar(baseBar);
if(!IsValidIndex(leftBar))
leftBar = baseBar;
double fullTop, fullBottom, refinedTop, refinedBottom;
BuildSupplyZone(leftBar, baseBar, i, fullTop, fullBottom, refinedTop, refinedBottom);
bool hasMomentum = true;
bool hasLiquidity = HasBearLiquidityContext(baseBar, i);
bool hasImbalance = HasBearImbalanceAround(i);
int score = ScoreZone(hasBos, hasMomentum, hasLiquidity, hasImbalance);
if(!PassesFilters(hasBos, hasMomentum, hasLiquidity, hasImbalance, score))
continue;
if(ZoneExists(ZONE_SUPPLY, baseBar, i, fullTop, fullBottom))
continue;
SupplyDemandZone z;
z.id = nextZoneId++;
z.side = ZONE_SUPPLY;
z.type = ClassifySupplyType(baseBar, i);
z.state = ZONE_FRESH;
z.leftBar = leftBar;
z.baseBar = baseBar;
z.breakBar = i;
z.rightBar = i;
z.fullTop = fullTop;
z.fullBottom = fullBottom;
z.refinedTop = refinedTop;
z.refinedBottom = refinedBottom;
z.hasBos = hasBos;
z.hasMomentum = hasMomentum;
z.hasLiquidity = hasLiquidity;
z.hasImbalance = hasImbalance;
z.isInducement = false;
z.qualityScore = score;
z.touchBar = -1;
z.mitigateBar = -1;
z.invalidateBar = -1;
PushZone(z);
}
}
//========================
// Inducement tagging
//========================
void MarkInducementZones() {
for(int i = 0; i < ArraySize(zones); i++) {
zones[i].isInducement = false;
}
for(int i = 0; i < ArraySize(zones); i++) {
for(int j = 0; j < ArraySize(zones); j++) {
if(i == j)
continue;
if(zones[i].side != zones[j].side)
continue;
if(zones[i].state == ZONE_INVALID || zones[j].state == ZONE_INVALID)
continue;
if(zones[i].side == ZONE_DEMAND) {
if(zones[i].baseBar < zones[j].baseBar &&
zones[i].fullBottom > zones[j].fullBottom &&
zones[i].qualityScore < zones[j].qualityScore) {
zones[i].isInducement = true;
}
} else {
if(zones[i].baseBar < zones[j].baseBar &&
zones[i].fullTop < zones[j].fullTop &&
zones[i].qualityScore < zones[j].qualityScore) {
zones[i].isInducement = true;
}
}
}
}
}
//========================
// Lifecycle
//========================
void UpdateZoneState() {
int end = Bars() - 1;
for(int i = 0; i < ArraySize(zones); i++) {
if(zones[i].state == ZONE_INVALID)
continue;
for(int b = zones[i].breakBar + 1; b <= end; b++) {
bool touched = PriceTouchesZone(b, zones[i].fullTop, zones[i].fullBottom);
bool reached50 = false;
bool invalid = false;
if(zones[i].side == ZONE_DEMAND) {
reached50 = LowAt(b) <= ZoneMid(zones[i].fullTop, zones[i].fullBottom);
invalid = PriceClosesBeyondDemand(b, zones[i].fullBottom);
} else {
reached50 = HighAt(b) >= ZoneMid(zones[i].fullTop, zones[i].fullBottom);
invalid = PriceClosesBeyondSupply(b, zones[i].fullTop);
}
if(touched && zones[i].touchBar < 0) {
zones[i].touchBar = b;
if(zones[i].state == ZONE_FRESH)
zones[i].state = ZONE_TOUCHED;
}
if(reached50 && zones[i].mitigateBar < 0) {
zones[i].mitigateBar = b;
if(zones[i].state != ZONE_INVALID)
zones[i].state = ZONE_MITIGATED;
}
if(invalid) {
zones[i].invalidateBar = b;
zones[i].state = ZONE_INVALID;
break;
}
}
}
}
//========================
// Draw
//========================
string TypeText(EZoneType type) {
if(type == ZONE_EXTREME) return "EXT";
if(type == ZONE_DECISIONAL) return "DEC";
return "OB";
}
string StateText(EZoneState state) {
if(state == ZONE_FRESH) return "FRESH";
if(state == ZONE_TOUCHED) return "TOUCHED";
if(state == ZONE_MITIGATED) return "MITIGATED";
return "INVALID";
}
color ZoneColor(const SupplyDemandZone &z) {
if(z.state == ZONE_INVALID)
return clrSilver;
if(z.state == ZONE_MITIGATED)
return clrDarkGray;
if(z.side == ZONE_DEMAND)
return clrPaleGreen;
return clrTomato;
}
string ZoneText(const SupplyDemandZone &z) {
string txt = (z.side == ZONE_DEMAND ? "DEMAND " : "SUPPLY ");
txt += TypeText(z.type);
txt += " ";
txt += StateText(z.state);
if(z.isInducement)
txt += " IDM";
if(z.hasBos) txt += " BOS";
if(z.hasMomentum) txt += " MOM";
if(z.hasLiquidity) txt += " LIQ";
if(z.hasImbalance) txt += " IMB";
return txt;
}
void DrawZones() {
int end = Bars() - 1;
for(int i = 0; i < ArraySize(zones); i++) {
SupplyDemandZone z = zones[i];
if(ch4_hideInvalidZones && z.state == ZONE_INVALID)
continue;
int toIndex = end;
if(z.invalidateBar >= 0) toIndex = z.invalidateBar;
color fullColor = ZoneColor(z);
string txt = ZoneText(z);
DrawZone(
K_ZONE(z.id),
z.leftBar,
toIndex,
z.fullTop,
z.fullBottom,
txt,
fullColor,
0.06,
6,
false
);
DrawZone(
K_ZONE_R(z.id),
z.leftBar,
toIndex,
z.refinedTop,
z.refinedBottom,
"",
(z.side == ZONE_DEMAND ? clrLime : clrRed),
0.16,
5,
true
);
}
}
public:
C_Chapter4() {
nextZoneId = 1;
protectedHigh = -1;
protectedLow = -1;
ArrayResize(zones, 0);
}
~C_Chapter4() {}
void SetExternalRange(int protectedHighIndex, int protectedLowIndex) {
protectedHigh = protectedHighIndex;
protectedLow = protectedLowIndex;
}
int GetZonesCount() const {
return ArraySize(zones);
}
bool GetZone(int index, SupplyDemandZone &outZone) {
if(index < 0 || index >= ArraySize(zones))
return false;
outZone = zones[index];
return true;
}
int OnInit(string s, ENUM_TIMEFRAMES tf_, int lr = 3, int look = 1) {
timeframe = tf_;
symbol = s;
debug.Init(0, "Chapter4", true);
if(swing.Init("Fractal", s, tf_, lr, look) == -1) {
Print("Error creating Swing Indicator: ", GetLastError());
return INIT_FAILED;
}
if(fairValue.Init("Fair Value Gaps", s, tf_, lr, look) == -1) {
Print("Error creating Fair Value indicator: ", GetLastError());
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}
void OnCalculate() {
if(!OnNewBar())
return;
UpdateBars(3);
ArrayResize(zones, 0);
nextZoneId = 1;
DetectDemandZones();
DetectSupplyZones();
MarkInducementZones();
UpdateZoneState();
DrawZones();
}
};
//+------------------------------------------------------------------+