//+------------------------------------------------------------------+ //| 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(); } }; //+------------------------------------------------------------------+