StopFirst-Core/Include/StopFirst/Bars.mqh
RUSLAN SLOBODIANIUK 6ba79702fd Make the header comments self-contained
They referred to project documents that are not part of this repository.
Comments only; PriceToPips is now documented as not rounding, which is what
it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:48:01 +03:00

276 lines
12 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+