Centaur_Quant_Architecture/MQL5/Include/Execution/COrderBlockScanner.mqh

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