360 lines
17 KiB
MQL5
360 lines
17 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| COrderBlockScanner.mqh |
|
|
//| Centaur Quant Architecture — Execution Module |
|
|
//| Strict SMC Order Block + FVG Zone Scanner |
|
|
//+------------------------------------------------------------------+
|
|
//| PURPOSE |
|
|
//| Identifies recent Bullish/Bearish Order Blocks that are backed |
|
|
//| by a clear Fair Value Gap (FVG / imbalance) created by an |
|
|
//| expanding displacement candle. The zone must be UNMITIGATED |
|
|
//| (price never retraced into it since formation) and the CURRENT |
|
|
//| live price must be approaching/tapping it within an ATR-based |
|
|
//| proximity buffer (via CSymbolNormalizer). |
|
|
//| The scanner only DETECTS; the caller feeds the returned |
|
|
//| coordinates (entry/sl/tp) into CSDPEncoder::EncodeSetup. |
|
|
//| Scan order: newest -> oldest, bounded by depth (default 200). |
|
|
//+------------------------------------------------------------------+
|
|
#property strict
|
|
|
|
#ifndef ORDERBLOCKSCANNER_MQH
|
|
#define ORDERBLOCKSCANNER_MQH
|
|
|
|
#include "../Core/CSymbolNormalizer.mqh"
|
|
|
|
//--- structured zone output consumed by the main EA ---
|
|
struct SOrderBlockZone
|
|
{
|
|
bool valid; // true when a trigger zone was produced
|
|
bool is_bullish; // true = buy zone, false = sell zone
|
|
int ob_index; // bar index of the order block candle (0 = current)
|
|
datetime ob_time; // open time of the order block candle
|
|
double zone_low; // lower edge of the OB/FVG zone
|
|
double zone_high; // upper edge of the OB/FVG zone
|
|
double entry; // proposed entry (tick-snapped)
|
|
double sl; // proposed stop-loss (tick-snapped)
|
|
double tp; // proposed take-profit (tick-snapped)
|
|
double fvg_size; // size of the fair value gap in price terms
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
class COrderBlockScanner
|
|
{
|
|
private:
|
|
string m_symbol; // instrument scanned
|
|
ENUM_TIMEFRAMES m_period; // timeframe scanned
|
|
CSymbolNormalizer *m_normalizer; // non-owning ATR/proximity source
|
|
bool m_ready; // valid symbol + normalizer
|
|
|
|
//--- mitigation: any completed bar AFTER the setup traded into the zone? ---
|
|
bool IsBullishMitigated(const double &high[], const double &low[], const int ob_index);
|
|
bool IsBearishMitigated(const double &high[], const double &low[], const int ob_index);
|
|
|
|
public:
|
|
COrderBlockScanner(CSymbolNormalizer *normalizer,
|
|
const string symbol = "",
|
|
const ENUM_TIMEFRAMES period = PERIOD_CURRENT);
|
|
~COrderBlockScanner();
|
|
|
|
//--- scan newest -> oldest for the freshest trigger; false when none ---
|
|
bool Scan(SOrderBlockZone &out_zone,
|
|
const int depth = 200,
|
|
const double expansion_min = 1.2,
|
|
const double fvg_min_atr_ratio = 0.0,
|
|
const int atr_period = 14,
|
|
const double proximity_atr_multiplier = 0.25,
|
|
const double sl_atr_multiplier = 0.25,
|
|
const double risk_reward = 2.0,
|
|
const bool allow_mitigated = false);
|
|
|
|
//--- read access ---
|
|
bool IsReady() const { return m_ready; }
|
|
string Symbol() const { return m_symbol; }
|
|
string Timeframe() const;
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
//| Constructor — bind normalizer (non-owning) and resolve symbol. |
|
|
//+------------------------------------------------------------------+
|
|
COrderBlockScanner::COrderBlockScanner(CSymbolNormalizer *normalizer,
|
|
const string symbol,
|
|
const ENUM_TIMEFRAMES period)
|
|
: m_symbol(symbol),
|
|
m_period(period),
|
|
m_normalizer(normalizer),
|
|
m_ready(false)
|
|
{
|
|
if(m_normalizer == NULL)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: null CSymbolNormalizer pointer.");
|
|
return;
|
|
}
|
|
if(StringLen(m_symbol) == 0)
|
|
m_symbol = m_normalizer.Symbol(); // inherit the normalizer's instrument
|
|
if(StringLen(m_symbol) == 0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: no symbol available (normalizer not bound).");
|
|
return;
|
|
}
|
|
m_ready = m_normalizer.IsReady();
|
|
if(!m_ready)
|
|
PrintFormat("[COrderBlockScanner] ERROR: underlying normalizer not ready for %s.", m_symbol);
|
|
else
|
|
PrintFormat("[COrderBlockScanner] INFO: ready on %s %s.", m_symbol, Timeframe());
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Destructor — non-owning pointer; nothing to release. |
|
|
//+------------------------------------------------------------------+
|
|
COrderBlockScanner::~COrderBlockScanner()
|
|
{
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Scan — single bounded pass over the OHLC window. |
|
|
//| Index 0 = current (forming) bar; larger indices = older bars. |
|
|
//| Pattern (bullish shown; bearish mirrored): |
|
|
//| i : order block candle (down candle) |
|
|
//| i-1 : expanding displacement candle (up, closes above high[i])|
|
|
//| i-2 : confirm candle — low[i-2] > high[i] => FVG opens |
|
|
//| Zone : [high[i], low[i-2]] (bullish) |
|
|
//| Trigger: zone unmitigated AND live price within ATR proximity. |
|
|
//+------------------------------------------------------------------+
|
|
bool COrderBlockScanner::Scan(SOrderBlockZone &out_zone,
|
|
const int depth,
|
|
const double expansion_min,
|
|
const double fvg_min_atr_ratio,
|
|
const int atr_period,
|
|
const double proximity_atr_multiplier,
|
|
const double sl_atr_multiplier,
|
|
const double risk_reward,
|
|
const bool allow_mitigated)
|
|
{
|
|
out_zone.valid = false;
|
|
//--- rigid input validation ---
|
|
if(!m_ready || m_normalizer == NULL)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: scanner not ready.");
|
|
return false;
|
|
}
|
|
if(depth < 5)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: depth %d < minimum 5.", depth);
|
|
return false;
|
|
}
|
|
if(expansion_min < 1.0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: expansion_min must be >= 1.0 (got %G).", expansion_min);
|
|
return false;
|
|
}
|
|
if(fvg_min_atr_ratio < 0.0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: fvg_min_atr_ratio must be >= 0 (got %G).", fvg_min_atr_ratio);
|
|
return false;
|
|
}
|
|
if(proximity_atr_multiplier <= 0.0 || sl_atr_multiplier <= 0.0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: ATR multipliers must be > 0 (prox=%G sl=%G).",
|
|
proximity_atr_multiplier, sl_atr_multiplier);
|
|
return false;
|
|
}
|
|
if(risk_reward <= 0.0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: risk_reward must be > 0 (got %G).", risk_reward);
|
|
return false;
|
|
}
|
|
int bars = depth;
|
|
if(bars > 10000)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] WARNING: depth %d clamped to 10000.", depth);
|
|
bars = 10000;
|
|
}
|
|
//--- dynamic ATR-based buffers from the normalizer ---
|
|
const double proximity_buffer = m_normalizer.GetATRBuffer(atr_period, proximity_atr_multiplier);
|
|
const double sl_buffer = m_normalizer.GetATRBuffer(atr_period, sl_atr_multiplier);
|
|
if(proximity_buffer <= 0.0 || sl_buffer <= 0.0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: ATR buffers unavailable for %s (warm-up?). Scan aborted.",
|
|
m_symbol);
|
|
return false;
|
|
}
|
|
// raw ATR recovered from the buffered value for the optional gap filter
|
|
const double raw_atr = proximity_buffer / proximity_atr_multiplier;
|
|
const double min_fvg = fvg_min_atr_ratio * raw_atr;
|
|
//--- one window copy; index 0 = most recent bar (series orientation) ---
|
|
double open[], high[], low[], close[];
|
|
datetime time[];
|
|
ArraySetAsSeries(open, true); // EXP-003A: perbaikan orientasi seri (bug deteksi 0)
|
|
ArraySetAsSeries(high, true);
|
|
ArraySetAsSeries(low, true);
|
|
ArraySetAsSeries(close, true);
|
|
ArraySetAsSeries(time, true);
|
|
const int n_open = CopyOpen(m_symbol, m_period, 0, bars, open);
|
|
const int n_high = CopyHigh(m_symbol, m_period, 0, bars, high);
|
|
const int n_low = CopyLow(m_symbol, m_period, 0, bars, low);
|
|
const int n_close = CopyClose(m_symbol, m_period, 0, bars, close);
|
|
const int n_time = CopyTime(m_symbol, m_period, 0, bars, time);
|
|
const int n = MathMin(MathMin(n_open, n_high), MathMin(MathMin(n_low, n_close), n_time));
|
|
if(n < 5)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: insufficient history for %s %s (got %d bars).",
|
|
m_symbol, Timeframe(), n);
|
|
return false;
|
|
}
|
|
//--- live price for the proximity trigger ---
|
|
MqlTick tick;
|
|
if(!SymbolInfoTick(m_symbol, tick) || tick.bid <= 0.0 || tick.ask <= 0.0)
|
|
{
|
|
PrintFormat("[COrderBlockScanner] ERROR: no live tick for %s. Scan aborted.", m_symbol);
|
|
return false;
|
|
}
|
|
//--- newest OB (i=3) -> oldest: first trigger wins (freshest zone) ---
|
|
for(int i = 3; i < n; i++)
|
|
{
|
|
const double ob_range = high[i] - low[i];
|
|
const double imp_range = high[i - 1] - low[i - 1];
|
|
if(ob_range <= 0.0 || imp_range < expansion_min * ob_range)
|
|
continue; // not an expanding displacement move
|
|
//================ BULLISH OB + FVG ================
|
|
if(close[i] < open[i] && close[i - 1] > open[i - 1] && close[i - 1] > high[i])
|
|
{
|
|
// the expanding up candle opened a gap above the OB high
|
|
if(low[i - 2] > high[i])
|
|
{
|
|
const double fvg_size = low[i - 2] - high[i];
|
|
if(fvg_min_atr_ratio <= 0.0 || fvg_size >= min_fvg)
|
|
{
|
|
if(!allow_mitigated && !IsBullishMitigated(high, low, i))
|
|
{
|
|
const double zone_low = high[i];
|
|
const double zone_high = low[i - 2];
|
|
// price approaching/tapping from above (bid-side)
|
|
if(tick.bid >= zone_low - proximity_buffer &&
|
|
tick.bid <= zone_high + proximity_buffer)
|
|
{
|
|
const double entry = m_normalizer.NormalizePrice(zone_low);
|
|
const double sl = m_normalizer.NormalizePrice(low[i] - sl_buffer);
|
|
const double risk = entry - sl;
|
|
if(entry > 0.0 && sl > 0.0 && risk > 0.0)
|
|
{
|
|
const double tp = m_normalizer.NormalizePrice(entry + risk * risk_reward);
|
|
if(tp > entry)
|
|
{
|
|
out_zone.valid = true;
|
|
out_zone.is_bullish = true;
|
|
out_zone.ob_index = i;
|
|
out_zone.ob_time = time[i];
|
|
out_zone.zone_low = zone_low;
|
|
out_zone.zone_high = zone_high;
|
|
out_zone.entry = entry;
|
|
out_zone.sl = sl;
|
|
out_zone.tp = tp;
|
|
out_zone.fvg_size = fvg_size;
|
|
PrintFormat("[COrderBlockScanner] INFO: Bullish OB+FVG @ %s bar %d (%.5f..%.5f), entry %.5f sl %.5f tp %.5f.",
|
|
m_symbol, i, zone_low, zone_high, entry, sl, tp);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//================ BEARISH OB + FVG ================
|
|
else
|
|
if(close[i] > open[i] && close[i - 1] < open[i - 1] && close[i - 1] < low[i])
|
|
{
|
|
// the expanding down candle opened a gap below the OB low
|
|
if(high[i - 2] < low[i])
|
|
{
|
|
const double fvg_size = low[i] - high[i - 2];
|
|
if(fvg_min_atr_ratio <= 0.0 || fvg_size >= min_fvg)
|
|
{
|
|
if(!allow_mitigated && !IsBearishMitigated(high, low, i))
|
|
{
|
|
const double zone_low = high[i - 2];
|
|
const double zone_high = low[i];
|
|
// price approaching/tapping from below (ask-side)
|
|
if(tick.ask >= zone_low - proximity_buffer &&
|
|
tick.ask <= zone_high + proximity_buffer)
|
|
{
|
|
const double entry = m_normalizer.NormalizePrice(zone_high);
|
|
const double sl = m_normalizer.NormalizePrice(high[i] + sl_buffer);
|
|
const double risk = sl - entry;
|
|
if(entry > 0.0 && sl > 0.0 && risk > 0.0)
|
|
{
|
|
const double tp = m_normalizer.NormalizePrice(entry - risk * risk_reward);
|
|
if(tp > 0.0 && tp < entry)
|
|
{
|
|
out_zone.valid = true;
|
|
out_zone.is_bullish = false;
|
|
out_zone.ob_index = i;
|
|
out_zone.ob_time = time[i];
|
|
out_zone.zone_low = zone_low;
|
|
out_zone.zone_high = zone_high;
|
|
out_zone.entry = entry;
|
|
out_zone.sl = sl;
|
|
out_zone.tp = tp;
|
|
out_zone.fvg_size = fvg_size;
|
|
PrintFormat("[COrderBlockScanner] INFO: Bearish OB+FVG @ %s bar %d (%.5f..%.5f), entry %.5f sl %.5f tp %.5f.",
|
|
m_symbol, i, zone_low, zone_high, entry, sl, tp);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//--- no proximity-triggered zone within the window (normal outcome) ---
|
|
static ulong s_last_no_zone_log_ms = 0; // throttle anti-spam: wall-clock GetTickCount64 (TimeCurrent() melaju cepat di tester)
|
|
const ulong now_ms = GetTickCount64();
|
|
if(now_ms - s_last_no_zone_log_ms >= 10000)
|
|
{
|
|
s_last_no_zone_log_ms = now_ms;
|
|
PrintFormat("[COrderBlockScanner] INFO: no unmitigated OB+FVG in proximity within %d bars on %s %s.",
|
|
n, m_symbol, Timeframe());
|
|
}
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| IsBullishMitigated — true when any COMPLETED bar after the setup |
|
|
//| traded at/below the zone top (gap filled). Bar 0 (forming) and |
|
|
//| the impulse/confirm candles are excluded: the trigger bar may |
|
|
//| legitimately be the one tapping the zone right now. |
|
|
//+------------------------------------------------------------------+
|
|
bool COrderBlockScanner::IsBullishMitigated(const double &high[], const double &low[],
|
|
const int ob_index)
|
|
{
|
|
const double zone_top = high[ob_index];
|
|
for(int j = 1; j <= ob_index - 3; j++)
|
|
if(low[j] <= zone_top)
|
|
return true;
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| IsBearishMitigated — symmetric: any completed bar traded at/above |
|
|
//| the zone bottom since the setup formed. |
|
|
//+------------------------------------------------------------------+
|
|
bool COrderBlockScanner::IsBearishMitigated(const double &high[], const double &low[],
|
|
const int ob_index)
|
|
{
|
|
const double zone_bottom = low[ob_index];
|
|
for(int j = 1; j <= ob_index - 3; j++)
|
|
if(high[j] >= zone_bottom)
|
|
return true;
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Timeframe — wire name of the scanned period. |
|
|
//+------------------------------------------------------------------+
|
|
string COrderBlockScanner::Timeframe() const
|
|
{
|
|
ENUM_TIMEFRAMES p = m_period;
|
|
if(p == PERIOD_CURRENT)
|
|
p = (ENUM_TIMEFRAMES)_Period;
|
|
string name = EnumToString(p);
|
|
StringReplace(name, "PERIOD_", "");
|
|
return name;
|
|
}
|
|
|
|
#endif // ORDERBLOCKSCANNER_MQH
|