forked from LengKundee/MQL5-Google-Onedrive
348 lines
12 KiB
MQL5
348 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| SMC_TrendBreakout_MTF.mq5 |
|
|
//| Visual indicator: BOS/CHoCH + Donchian breakout + MTF filter |
|
|
//+------------------------------------------------------------------+
|
|
#property strict
|
|
#property indicator_chart_window
|
|
#property indicator_buffers 2
|
|
#property indicator_plots 2
|
|
|
|
// --- plot 0: buy arrows
|
|
#property indicator_label1 "Long"
|
|
#property indicator_type1 DRAW_ARROW
|
|
#property indicator_color1 clrLimeGreen
|
|
#property indicator_style1 STYLE_SOLID
|
|
#property indicator_width1 2
|
|
// --- plot 1: sell arrows
|
|
#property indicator_label2 "Short"
|
|
#property indicator_type2 DRAW_ARROW
|
|
#property indicator_color2 clrTomato
|
|
#property indicator_style2 STYLE_SOLID
|
|
#property indicator_width2 2
|
|
|
|
input group "SMC (structure)"
|
|
input bool UseSMC = true;
|
|
input bool UseCHoCH = true; // label opposite break as CHoCH
|
|
|
|
input group "Trend Breakout"
|
|
input bool UseDonchianBreakout = true;
|
|
input int DonchianLookback = 20; // bars (completed bars)
|
|
|
|
input group "Lower timeframe confirmation"
|
|
input bool RequireMTFConfirm = true;
|
|
input ENUM_TIMEFRAMES LowerTF = PERIOD_M5;
|
|
input int EMAFast = 20;
|
|
input int EMASlow = 50;
|
|
|
|
input group "Signals"
|
|
input bool FireOnClose = true; // use last closed bar (recommended)
|
|
input int ArrowCodeLong = 233; // wingdings: up arrow
|
|
input int ArrowCodeShort = 234; // wingdings: down arrow
|
|
input int ArrowOffsetPoints = 10; // vertical offset in points
|
|
|
|
input group "Drawing"
|
|
input bool DrawStructureLines = true;
|
|
input bool DrawBreakoutLines = false;
|
|
input int MaxObjects = 200; // prevent clutter
|
|
|
|
input group "Notifications"
|
|
input bool PopupAlerts = false;
|
|
input bool PushNotifications = false;
|
|
|
|
double gLongBuf[];
|
|
double gShortBuf[];
|
|
|
|
int gFractalsHandle = INVALID_HANDLE;
|
|
int gEmaFastHandle = INVALID_HANDLE;
|
|
int gEmaSlowHandle = INVALID_HANDLE;
|
|
|
|
// Tracks last trend direction on main TF: 1 bullish, -1 bearish, 0 unknown
|
|
int gTrendDir = 0;
|
|
|
|
string gObjPrefix;
|
|
|
|
datetime gLastBarTime = 0;
|
|
|
|
static int ClampInt(const int value, const int lowerBound, const int upperBound) { return (value<lowerBound?lowerBound:(value>upperBound?upperBound:value)); }
|
|
static bool IsNewBar(const datetime t0) { if(t0==gLastBarTime) return false; gLastBarTime=t0; return true; }
|
|
|
|
static void Notify(const string msg)
|
|
{
|
|
if(PopupAlerts) Alert(msg);
|
|
if(PushNotifications) SendNotification(msg);
|
|
}
|
|
|
|
static void SafeDeleteOldObjects()
|
|
{
|
|
// keep last MaxObjects objects with prefix, delete oldest (by time suffix)
|
|
// OPTIMIZATION: Single-pass algorithm instead of double-loop
|
|
int total = ObjectsTotal(0, 0, -1);
|
|
int objectCount = 0;
|
|
|
|
// Single pass: count and store object names
|
|
string objectNames[];
|
|
ArrayResize(objectNames, 0);
|
|
|
|
for(int objectIndex=total-1;objectIndex>=0;objectIndex--)
|
|
{
|
|
string name = ObjectName(0, objectIndex, 0, -1);
|
|
if(StringFind(name, gObjPrefix) == 0)
|
|
{
|
|
// OPTIMIZATION: Store size to avoid repeated ArraySize() calls
|
|
int currentSize = ArraySize(objectNames);
|
|
ArrayResize(objectNames, currentSize + 1);
|
|
objectNames[currentSize] = name;
|
|
objectCount++;
|
|
}
|
|
}
|
|
|
|
// If within limit, no deletion needed
|
|
if(objectCount <= MaxObjects) return;
|
|
|
|
// Delete all objects with prefix (fast & safe for indicator)
|
|
for(int i = 0; i < ArraySize(objectNames); i++)
|
|
{
|
|
ObjectDelete(0, objectNames[i]);
|
|
}
|
|
}
|
|
|
|
static void DrawHLine(const string name, const double price, const color lineColor, const ENUM_LINE_STYLE lineStyle, const int lineWidth)
|
|
{
|
|
if(!DrawStructureLines && !DrawBreakoutLines) return;
|
|
if(ObjectFind(0, name) >= 0) return;
|
|
ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, lineColor);
|
|
ObjectSetInteger(0, name, OBJPROP_STYLE, lineStyle);
|
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, lineWidth);
|
|
ObjectSetInteger(0, name, OBJPROP_BACK, true);
|
|
}
|
|
|
|
static void DrawText(const string name, const datetime timeValue, const double price, const string text, const color textColor)
|
|
{
|
|
if(ObjectFind(0, name) >= 0) return;
|
|
ObjectCreate(0, name, OBJ_TEXT, 0, timeValue, price);
|
|
ObjectSetString(0, name, OBJPROP_TEXT, text);
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
|
|
ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT);
|
|
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
|
|
ObjectSetInteger(0, name, OBJPROP_BACK, true);
|
|
}
|
|
|
|
static double HighestHigh(const double &high[], const int start, const int count)
|
|
{
|
|
int maxIndex = ArrayMaximum(high, start, count);
|
|
if(maxIndex == -1) return -DBL_MAX;
|
|
return high[maxIndex];
|
|
}
|
|
|
|
static double LowestLow(const double &low[], const int start, const int count)
|
|
{
|
|
int minIndex = ArrayMinimum(low, start, count);
|
|
if(minIndex == -1) return DBL_MAX;
|
|
return low[minIndex];
|
|
}
|
|
|
|
// --- Cached MTF direction (performance)
|
|
static datetime g_mtfDir_lastCheckTime = 0;
|
|
static int g_mtfDir_cachedValue = 0;
|
|
|
|
static int GetMTFDir()
|
|
{
|
|
if(!RequireMTFConfirm) return 0;
|
|
if(gEmaFastHandle==INVALID_HANDLE || gEmaSlowHandle==INVALID_HANDLE) return 0;
|
|
|
|
// PERF: Only check for new MTF direction on a new bar of the LowerTF.
|
|
datetime mtf_time[1];
|
|
if(CopyTime(_Symbol, LowerTF, 0, 1, mtf_time) != 1) return 0;
|
|
if(mtf_time[0] == g_mtfDir_lastCheckTime) return g_mtfDir_cachedValue;
|
|
g_mtfDir_lastCheckTime = mtf_time[0];
|
|
|
|
double fast[2], slow[2];
|
|
ArraySetAsSeries(fast, true);
|
|
ArraySetAsSeries(slow, true);
|
|
|
|
if(CopyBuffer(gEmaFastHandle, 0, 1, 1, fast) != 1) { g_mtfDir_cachedValue=0; return 0; }
|
|
if(CopyBuffer(gEmaSlowHandle, 0, 1, 1, slow) != 1) { g_mtfDir_cachedValue=0; return 0; }
|
|
|
|
if(fast[0] > slow[0]) g_mtfDir_cachedValue = 1;
|
|
else if(fast[0] < slow[0]) g_mtfDir_cachedValue = -1;
|
|
else g_mtfDir_cachedValue = 0;
|
|
|
|
return g_mtfDir_cachedValue;
|
|
}
|
|
|
|
int OnInit()
|
|
{
|
|
SetIndexBuffer(0, gLongBuf, INDICATOR_DATA);
|
|
SetIndexBuffer(1, gShortBuf, INDICATOR_DATA);
|
|
ArraySetAsSeries(gLongBuf, true);
|
|
ArraySetAsSeries(gShortBuf, true);
|
|
|
|
PlotIndexSetInteger(0, PLOT_ARROW, ArrowCodeLong);
|
|
PlotIndexSetInteger(1, PLOT_ARROW, ArrowCodeShort);
|
|
|
|
gObjPrefix = StringFormat("SMC_TB_MTF_%I64u_", (long)ChartID());
|
|
|
|
// fractals on main TF
|
|
gFractalsHandle = iFractals(_Symbol, _Period);
|
|
if(gFractalsHandle == INVALID_HANDLE) return INIT_FAILED;
|
|
|
|
// EMA handles on lower TF (for confirmation)
|
|
gEmaFastHandle = iMA(_Symbol, LowerTF, EMAFast, 0, MODE_EMA, PRICE_CLOSE);
|
|
gEmaSlowHandle = iMA(_Symbol, LowerTF, EMASlow, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
void OnDeinit(const int reason)
|
|
{
|
|
if(gFractalsHandle != INVALID_HANDLE) IndicatorRelease(gFractalsHandle);
|
|
if(gEmaFastHandle != INVALID_HANDLE) IndicatorRelease(gEmaFastHandle);
|
|
if(gEmaSlowHandle != INVALID_HANDLE) IndicatorRelease(gEmaSlowHandle);
|
|
}
|
|
|
|
int OnCalculate(
|
|
const int rates_total,
|
|
const int prev_calculated,
|
|
const datetime &time[],
|
|
const double &open[],
|
|
const double &high[],
|
|
const double &low[],
|
|
const double &close[],
|
|
const long &tick_volume[],
|
|
const long &volume[],
|
|
const int &spread[])
|
|
{
|
|
// OPTIMIZATION: Early exit if no new bars to calculate
|
|
if(prev_calculated > 0 && prev_calculated == rates_total)
|
|
return rates_total;
|
|
|
|
if(rates_total < 100) return 0;
|
|
int donLookback = (DonchianLookback < 2 ? 2 : DonchianLookback);
|
|
|
|
ArraySetAsSeries(time, true);
|
|
ArraySetAsSeries(open, true);
|
|
ArraySetAsSeries(high, true);
|
|
ArraySetAsSeries(low, true);
|
|
ArraySetAsSeries(close, true);
|
|
|
|
// Clear only newly calculated area; indicator buffers are series arrays (0 = current bar)
|
|
int start = (prev_calculated==0 ? rates_total-1 : rates_total - prev_calculated);
|
|
start = ClampInt(start, 0, rates_total-1);
|
|
for(int i=start;i>=0;i--)
|
|
{
|
|
gLongBuf[i] = EMPTY_VALUE;
|
|
gShortBuf[i] = EMPTY_VALUE;
|
|
}
|
|
|
|
// Only emit signals once per new bar (less spam); use bar 1 when FireOnClose.
|
|
if(!IsNewBar(time[0])) return rates_total;
|
|
|
|
SafeDeleteOldObjects();
|
|
|
|
const int sigBar = (FireOnClose ? 1 : 0);
|
|
if(sigBar >= rates_total-1) return rates_total;
|
|
|
|
// --- Pull fractal buffers for last ~500 bars for swing detection
|
|
int need = MathMin(600, rates_total);
|
|
double upFr[600], dnFr[600];
|
|
ArraySetAsSeries(upFr, true);
|
|
ArraySetAsSeries(dnFr, true);
|
|
|
|
int gotUp = CopyBuffer(gFractalsHandle, 0, 0, need, upFr);
|
|
int gotDn = CopyBuffer(gFractalsHandle, 1, 0, need, dnFr);
|
|
if(gotUp <= 0 || gotDn <= 0) return rates_total;
|
|
|
|
// Find most recent confirmed swing high/low (fractal appears 2 bars after formation)
|
|
double lastSwingHigh = 0.0; datetime lastSwingHighT = 0;
|
|
double lastSwingLow = 0.0; datetime lastSwingLowT = 0;
|
|
for(int i=sigBar+2; i<need; i++)
|
|
{
|
|
if(lastSwingHighT==0 && upFr[i] != 0.0) { lastSwingHigh = upFr[i]; lastSwingHighT = time[i]; }
|
|
if(lastSwingLowT==0 && dnFr[i] != 0.0) { lastSwingLow = dnFr[i]; lastSwingLowT = time[i]; }
|
|
if(lastSwingHighT!=0 && lastSwingLowT!=0) break;
|
|
}
|
|
|
|
// --- Determine Donchian breakout bounds (exclude current forming bar and signal bar)
|
|
int donStart = sigBar + 1;
|
|
int donCount = donLookback;
|
|
if(donStart + donCount >= rates_total) return rates_total;
|
|
|
|
double donHigh = HighestHigh(high, donStart, donCount);
|
|
double donLow = LowestLow(low, donStart, donCount);
|
|
|
|
// --- Lower TF confirmation
|
|
int mtfDir = GetMTFDir(); // 1 up, -1 down, 0 neutral/unknown
|
|
|
|
bool mtfOkLong = (!RequireMTFConfirm) || (mtfDir == 1);
|
|
bool mtfOkShort = (!RequireMTFConfirm) || (mtfDir == -1);
|
|
|
|
// --- Signals
|
|
bool smcLong=false, smcShort=false, donLong=false, donShort=false;
|
|
|
|
if(UseSMC)
|
|
{
|
|
if(lastSwingHighT!=0 && close[sigBar] > lastSwingHigh) smcLong = true;
|
|
if(lastSwingLowT!=0 && close[sigBar] < lastSwingLow) smcShort = true;
|
|
}
|
|
if(UseDonchianBreakout)
|
|
{
|
|
if(close[sigBar] > donHigh) donLong = true;
|
|
if(close[sigBar] < donLow) donShort = true;
|
|
}
|
|
|
|
bool finalLong = (smcLong || donLong) && mtfOkLong;
|
|
bool finalShort = (smcShort || donShort) && mtfOkShort;
|
|
|
|
// --- Plot & draw
|
|
double pointValue = _Point;
|
|
if(finalLong)
|
|
{
|
|
gLongBuf[sigBar] = low[sigBar] - ArrowOffsetPoints * pointValue;
|
|
|
|
color lineColor = clrLimeGreen;
|
|
if(smcLong && lastSwingHighT!=0 && DrawStructureLines)
|
|
{
|
|
int breakDir = 1;
|
|
bool choch = (UseCHoCH && gTrendDir!=0 && breakDir != gTrendDir);
|
|
string kind = (choch ? "CHoCH↑" : "BOS↑");
|
|
string structureObjectName = gObjPrefix + StringFormat("SMC_%s_%I64d", kind, (long)time[sigBar]);
|
|
DrawHLine(structureObjectName+"_L", lastSwingHigh, lineColor, STYLE_DOT, 1);
|
|
DrawText(structureObjectName+"_T", time[sigBar], lastSwingHigh, kind, lineColor);
|
|
gTrendDir = breakDir;
|
|
}
|
|
if(donLong && DrawBreakoutLines)
|
|
{
|
|
string breakoutObjectName = gObjPrefix + StringFormat("DON_H_%I64d", (long)time[sigBar]);
|
|
DrawHLine(breakoutObjectName, donHigh, clrDeepSkyBlue, STYLE_DASH, 1);
|
|
}
|
|
|
|
Notify(StringFormat("%s LONG | TF=%s | MTF=%s", _Symbol, EnumToString(_Period), EnumToString(LowerTF)));
|
|
}
|
|
if(finalShort)
|
|
{
|
|
gShortBuf[sigBar] = high[sigBar] + ArrowOffsetPoints * pointValue;
|
|
|
|
color lineColor = clrTomato;
|
|
if(smcShort && lastSwingLowT!=0 && DrawStructureLines)
|
|
{
|
|
int breakDir = -1;
|
|
bool choch = (UseCHoCH && gTrendDir!=0 && breakDir != gTrendDir);
|
|
string kind = (choch ? "CHoCH↓" : "BOS↓");
|
|
string structureObjectName = gObjPrefix + StringFormat("SMC_%s_%I64d", kind, (long)time[sigBar]);
|
|
DrawHLine(structureObjectName+"_L", lastSwingLow, lineColor, STYLE_DOT, 1);
|
|
DrawText(structureObjectName+"_T", time[sigBar], lastSwingLow, kind, lineColor);
|
|
gTrendDir = breakDir;
|
|
}
|
|
if(donShort && DrawBreakoutLines)
|
|
{
|
|
string breakoutObjectName = gObjPrefix + StringFormat("DON_L_%I64d", (long)time[sigBar]);
|
|
DrawHLine(breakoutObjectName, donLow, clrDeepSkyBlue, STYLE_DASH, 1);
|
|
}
|
|
|
|
Notify(StringFormat("%s SHORT | TF=%s | MTF=%s", _Symbol, EnumToString(_Period), EnumToString(LowerTF)));
|
|
}
|
|
|
|
return rates_total;
|
|
}
|
|
|