//+------------------------------------------------------------------+ //| Bars.mqh| //| Copyright 2026, bandersoft | //| https://www.mql5.com/en/users/fats | //+------------------------------------------------------------------+ //| | //| New-bar detection, bar extremes, candle patterns. | //| | //+------------------------------------------------------------------+ #ifndef STOPFIRST_BARS_MQH #define STOPFIRST_BARS_MQH #include "Defs.mqh" //+------------------------------------------------------------------+ //| Get highest high of last N bars | //| | //| Returns: the highest high in the window. 0 if the history does | //| not cover the whole window - indistinguishable from a | //| real price by value alone. | //+------------------------------------------------------------------+ double GetHighestHigh(int bars, int shift = 0, ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT, string symbol = "") { if(symbol == "") symbol = _Symbol; if(bars <= 0) return 0; double high[]; ArraySetAsSeries(high, true); //--- fewer bars than asked is not the window the caller named: a //--- partial copy returned the extreme of a shorter one (found in //--- review 2026-10-14) if(CopyHigh(symbol, timeframe, shift, bars, high) < bars) return 0; int maxIdx = ArrayMaximum(high); return high[maxIdx]; } //+------------------------------------------------------------------+ //| Get lowest low of last N bars | //| | //| Returns: the lowest low in the window. 0 if the history does | //| not cover the whole window - indistinguishable from a | //| real price by value alone. | //+------------------------------------------------------------------+ double GetLowestLow(int bars, int shift = 0, ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT, string symbol = "") { if(symbol == "") symbol = _Symbol; if(bars <= 0) return 0; double low[]; ArraySetAsSeries(low, true); if(CopyLow(symbol, timeframe, shift, bars, low) < bars) return 0; // partial = shorter window int minIdx = ArrayMinimum(low); return low[minIdx]; } //+------------------------------------------------------------------+ //| Detect candle pattern | //| Parameters: | //| pattern - which shape to test for | //| shift - bar to examine, 1 being the last closed bar | //| timeframe - PERIOD_CURRENT for the chart timeframe | //| symbol - empty for the current chart symbol | //| | //| Returns: an ENUM_SF_SIGNAL. | //| SF_SIGNAL_BULLISH / SF_SIGNAL_BEARISH - found, with a | //| direction. SF_SIGNAL_NEUTRAL - found, no direction, ie | //| a doji. SF_SIGNAL_NONE - not found, or no history. | //| | //| A doji returns SF_SIGNAL_NEUTRAL, not 0, so "found a doji" and | //| "found nothing" differ. And the body has a floor: on a bodyless | //| candle every "wick > body * n" test is trivially true, and one | //| doji would report as hammer, shooting star and pin bar at once - | //| a body under STOPFIRST_DOJI_BODY_RATIO of the range matches no | //| directional pattern. | //+------------------------------------------------------------------+ int DetectCandlePattern(ENUM_SF_PATTERN pattern, int shift = 1, ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT, string symbol = "") { if(symbol == "") symbol = _Symbol; if(shift < 0) return SF_SIGNAL_NONE; double open[], high[], low[], close[]; ArraySetAsSeries(open, true); ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); //--- two bars: the one under test and its predecessor, which //--- engulfing compares against //--- both bars or nothing: with "<= 0" a copy of ONE bar - the oldest //--- bar, or history still loading - would pass, and engulfing would //--- read [1], a critical array-out-of-range that stops the program if(CopyOpen(symbol, timeframe, shift, 2, open) < 2) return SF_SIGNAL_NONE; if(CopyHigh(symbol, timeframe, shift, 2, high) < 2) return SF_SIGNAL_NONE; if(CopyLow(symbol, timeframe, shift, 2, low) < 2) return SF_SIGNAL_NONE; if(CopyClose(symbol, timeframe, shift, 2, close) < 2) return SF_SIGNAL_NONE; double range = high[0] - low[0]; if(range <= 0) return SF_SIGNAL_NONE; // a flat bar has no shape double body = MathAbs(close[0] - open[0]); double bodyRatio = body / range; double upperWick = high[0] - MathMax(open[0], close[0]); double lowerWick = MathMin(open[0], close[0]) - low[0]; bool hasBody = (bodyRatio >= STOPFIRST_DOJI_BODY_RATIO); if(pattern == SF_PATTERN_DOJI) return hasBody ? SF_SIGNAL_NONE : SF_SIGNAL_NEUTRAL; //--- every remaining pattern is directional, and direction is //--- meaningless on a candle with no body if(!hasBody) return SF_SIGNAL_NONE; if(pattern == SF_PATTERN_HAMMER) { if(lowerWick > body * 2 && upperWick < body * 0.5) return SF_SIGNAL_BULLISH; } else if(pattern == SF_PATTERN_SHOOTING_STAR) { if(upperWick > body * 2 && lowerWick < body * 0.5) return SF_SIGNAL_BEARISH; } else if(pattern == SF_PATTERN_ENGULFING) { //--- the current body must cover the previous body outright. //--- comparing sizes alone would call any large candle after a //--- small opposite one an engulfing if(close[0] > open[0] && close[1] < open[1] && close[0] >= open[1] && open[0] <= close[1]) return SF_SIGNAL_BULLISH; if(close[0] < open[0] && close[1] > open[1] && close[0] <= open[1] && open[0] >= close[1]) return SF_SIGNAL_BEARISH; } else if(pattern == SF_PATTERN_PIN_BAR) { if(lowerWick > body * 2.5 && lowerWick > upperWick * 2) return SF_SIGNAL_BULLISH; if(upperWick > body * 2.5 && upperWick > lowerWick * 2) return SF_SIGNAL_BEARISH; } return SF_SIGNAL_NONE; } //+------------------------------------------------------------------+ //| Detects a new bar on one symbol and timeframe | //| | //| A free function IsNewBar() with a function-level static would be | //| shared by every caller: ask it about EURUSD and GBPUSD in the | //| same loop and each call overwrites the other's bar time, so both | //| report a new bar on every tick. | //| The state belongs to an object, one per series being watched. | //| | //| Usage - one member per series, initialised once: | //| | //| CNewBarDetector m1Bars; | //| int OnInit() { m1Bars.Init(_Symbol, PERIOD_M1); return 0; } | //| void OnTick() { if(m1Bars.IsNewBar()) ... } | //+------------------------------------------------------------------+ class CNewBarDetector { private: string m_symbol; ENUM_TIMEFRAMES m_timeframe; datetime m_lastBarTime; public: CNewBarDetector(void); void Init(const string symbol = "", const ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT); bool IsNewBar(void); datetime LastBarTime(void) const { return m_lastBarTime; } string Symbol(void) const { return m_symbol; } ENUM_TIMEFRAMES Timeframe(void) const { return m_timeframe; } void Reset(void) { m_lastBarTime = 0; } }; //+------------------------------------------------------------------+ //| Defaults to the current chart symbol and timeframe | //+------------------------------------------------------------------+ CNewBarDetector::CNewBarDetector(void) : m_symbol(_Symbol), m_timeframe(PERIOD_CURRENT), m_lastBarTime(0) { } //+------------------------------------------------------------------+ //| Point the detector at a series | //| Parameters: | //| symbol - empty for the current chart symbol | //| timeframe - PERIOD_CURRENT for the chart timeframe | //| Returns: nothing. Forgets any bar seen before. | //+------------------------------------------------------------------+ void CNewBarDetector::Init(const string symbol, const ENUM_TIMEFRAMES timeframe) { m_symbol = (symbol == "") ? _Symbol : symbol; m_timeframe = timeframe; m_lastBarTime = 0; } //+------------------------------------------------------------------+ //| Has a new bar opened since the last call? | //| | //| Returns: true exactly once per bar. false on the first call for | //| a series - that call only records where we are, and | //| treating startup as a bar change would fire every | //| strategy once for no reason. false if the series has no | //| data. | //+------------------------------------------------------------------+ bool CNewBarDetector::IsNewBar(void) { datetime currentBarTime = iTime(m_symbol, m_timeframe, 0); if(currentBarTime == 0) return false; if(m_lastBarTime == currentBarTime) return false; bool firstObservation = (m_lastBarTime == 0); m_lastBarTime = currentBarTime; return !firstObservation; } //+------------------------------------------------------------------+ //| Get time until next bar | //| | //| Returns: seconds until the current bar closes, never negative. | //| 0 if the series has no history, if the timeframe is | //| unknown, or if the bar is already overdue. | //+------------------------------------------------------------------+ int SecondsToNextBar(ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT, string symbol = "") { if(symbol == "") symbol = _Symbol; //--- a series with no history gives a bar time of 0, and the //--- result would be a large negative number datetime currentBarTime = iTime(symbol, timeframe, 0); if(currentBarTime == 0) return 0; int periodSeconds = PeriodSeconds(timeframe); if(periodSeconds <= 0) return 0; //--- PeriodSeconds(PERIOD_MN1) is a flat 30 days, so the 31st would //--- read 0 all day and February over-report by two. A monthly bar //--- ends at the first of the next month. datetime barEnd = currentBarTime + periodSeconds; ENUM_TIMEFRAMES resolved = (timeframe == PERIOD_CURRENT) ? (ENUM_TIMEFRAMES)Period() : timeframe; if(resolved == PERIOD_MN1) { MqlDateTime next; TimeToStruct(currentBarTime, next); next.mon++; if(next.mon > 12) { next.mon = 1; next.year++; } next.day = 1; next.hour = 0; next.min = 0; next.sec = 0; barEnd = StructToTime(next); } int remaining = (int)(barEnd - TimeCurrent()); //--- a bar can be overdue when ticks stop arriving; negative //--- seconds-until is not a meaningful answer return (remaining > 0) ? remaining : 0; } #endif // STOPFIRST_BARS_MQH //+------------------------------------------------------------------+