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>
276 lines
12 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|