2025-12-26 07:26:47 +00:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| SMC_TrendBreakout_MTF_EA.mq5 |
|
|
|
|
|
//| EA: SMC (BOS/CHoCH) + Donchian breakout + MTF confirmation |
|
|
|
|
|
//| Alerts / Push notifications + optional auto-trading |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#property strict
|
|
|
|
|
|
|
|
|
|
#include <Trade/Trade.mqh>
|
|
|
|
|
|
2025-12-26 08:17:39 +00:00
|
|
|
enum ENUM_SL_MODE
|
|
|
|
|
{
|
|
|
|
|
SL_ATR = 0, // ATR * multiplier
|
|
|
|
|
SL_SWING = 1, // last confirmed swing (fractal) + buffer
|
|
|
|
|
SL_FIXED_POINTS = 2 // fixed points
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
enum ENUM_TP_MODE
|
|
|
|
|
{
|
|
|
|
|
TP_RR = 0, // RR * SL distance
|
|
|
|
|
TP_FIXED_POINTS = 1, // fixed points
|
|
|
|
|
TP_DONCHIAN_WIDTH = 2 // Donchian channel width * multiplier
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
input group "Core"
|
|
|
|
|
input bool EnableTrading = false; // if false: alerts only
|
|
|
|
|
input long MagicNumber = 26012025;
|
|
|
|
|
input bool OnePositionPerSymbol = true;
|
|
|
|
|
|
|
|
|
|
input group "Main timeframe logic"
|
|
|
|
|
input ENUM_TIMEFRAMES SignalTF = PERIOD_CURRENT;
|
|
|
|
|
input bool FireOnClose = true; // use last closed bar on SignalTF
|
|
|
|
|
|
|
|
|
|
input group "SMC (structure)"
|
|
|
|
|
input bool UseSMC = true;
|
|
|
|
|
input bool UseCHoCH = true;
|
|
|
|
|
|
|
|
|
|
input group "Trend Breakout"
|
|
|
|
|
input bool UseDonchianBreakout = true;
|
|
|
|
|
input int DonchianLookback = 20;
|
|
|
|
|
|
|
|
|
|
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 "Risk / Orders"
|
2025-12-26 08:17:39 +00:00
|
|
|
input ENUM_SL_MODE SLMode = SL_ATR;
|
|
|
|
|
input ENUM_TP_MODE TPMode = TP_RR;
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
input double FixedLots = 0.10; // used when RiskPercent=0
|
|
|
|
|
input double RiskPercent = 0.0; // if >0: position size from SL distance
|
2025-12-26 08:17:39 +00:00
|
|
|
input bool RiskUseEquity = true; // recommended
|
|
|
|
|
input bool RiskClampToFreeMargin = true; // reduce lots if not enough margin
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
input int ATRPeriod = 14;
|
|
|
|
|
input double ATR_SL_Mult = 2.0;
|
2025-12-26 08:17:39 +00:00
|
|
|
|
|
|
|
|
input int SwingSLBufferPoints = 20; // extra points beyond swing (SL_SWING)
|
|
|
|
|
input int FixedSLPoints = 500; // SL_FIXED_POINTS
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
input double RR = 2.0;
|
2025-12-26 08:17:39 +00:00
|
|
|
input int FixedTPPoints = 1000; // TP_FIXED_POINTS
|
|
|
|
|
input double DonchianTP_Mult = 1.0; // TP_DONCHIAN_WIDTH
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
input int SlippagePoints = 30;
|
|
|
|
|
|
|
|
|
|
input group "Notifications"
|
|
|
|
|
input bool PopupAlerts = true;
|
|
|
|
|
input bool PushNotifications = true;
|
2026-01-04 20:16:44 +00:00
|
|
|
input bool EnableWebRequest = false; // enable custom web request
|
|
|
|
|
// IMPORTANT: URL must be added to MT5 terminal's allowed list:
|
|
|
|
|
// Tools -> Options -> Expert Advisors -> Allow WebRequest for listed URL
|
|
|
|
|
input string WebRequestURL = "https://jules.google.com/repo/github/Mouy-leng/ZOLO-A6-9VxNUNA-";
|
|
|
|
|
|
|
|
|
|
// --- Web request (for external integrations)
|
|
|
|
|
static void SendWebRequest(const string msg)
|
|
|
|
|
{
|
|
|
|
|
if(!EnableWebRequest || WebRequestURL == "") return;
|
|
|
|
|
|
|
|
|
|
char data[], result[];
|
|
|
|
|
string headers;
|
|
|
|
|
int timeout = 5000; // 5 seconds
|
|
|
|
|
|
|
|
|
|
// Simple JSON payload
|
|
|
|
|
string json = "{ \"event\": \"signal\", \"message\": \"" + msg + "\" }";
|
|
|
|
|
StringToCharArray(json, data, 0, StringLen(json), CP_UTF8);
|
|
|
|
|
|
|
|
|
|
ResetLastError();
|
|
|
|
|
int res = WebRequest("POST", WebRequestURL, "Content-Type: application/json", timeout, data, result, headers);
|
|
|
|
|
|
|
|
|
|
if(res == -1)
|
|
|
|
|
{
|
|
|
|
|
Print("WebRequest error: ", GetLastError());
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-26 07:26:47 +00:00
|
|
|
|
|
|
|
|
CTrade gTrade;
|
|
|
|
|
|
|
|
|
|
int gFractalsHandle = INVALID_HANDLE;
|
|
|
|
|
int gAtrHandle = INVALID_HANDLE;
|
|
|
|
|
int gEmaFastHandle = INVALID_HANDLE;
|
|
|
|
|
int gEmaSlowHandle = INVALID_HANDLE;
|
|
|
|
|
|
|
|
|
|
datetime gLastSignalBarTime = 0;
|
|
|
|
|
int gTrendDir = 0; // 1 bullish, -1 bearish, 0 unknown (for CHoCH labelling)
|
feat(performance): Add new bar check to OnTick()
💡 What:
This commit introduces a performance optimization to the `OnTick()` function in the main Expert Advisor. It adds a check at the very beginning of the function to determine if a new bar has formed on the signal timeframe.
🎯 Why:
The `OnTick()` function is executed on every incoming price tick, which can happen multiple times per second. The core trading logic, however, is based on analyzing closed bars. Without this check, the EA was performing expensive calculations (like `CopyRates`, indicator calculations, and loop-based analysis) on every single tick, even when no new trading information was available. This was highly inefficient and consumed unnecessary CPU resources.
📊 Impact:
This change dramatically reduces the EA's computational overhead. The expensive logic inside `OnTick()` will now run only once per bar, instead of on every tick. This leads to a significant reduction in CPU usage, making the EA run much more efficiently, especially in volatile markets.
🔬 Measurement:
The improvement can be verified by observing the EA's execution frequency in the MetaTrader 5 "Experts" tab. Before this change, the log would show frequent processing. After this change, processing logs will only appear once at the beginning of each new bar on the selected timeframe.
2026-01-05 17:04:38 +00:00
|
|
|
static datetime gLastTickBarTime = 0; // PERF: For new bar check
|
2025-12-26 07:26:47 +00:00
|
|
|
|
2025-12-27 19:05:05 +00:00
|
|
|
// --- Cached symbol properties (performance)
|
|
|
|
|
// Initialized once in OnInit to avoid repeated calls in OnTick.
|
|
|
|
|
static double G_POINT = 0.0;
|
|
|
|
|
static double G_TICK_SIZE = 0.0;
|
|
|
|
|
static double G_TICK_VALUE = 0.0;
|
|
|
|
|
static double G_VOL_MIN = 0.0;
|
|
|
|
|
static double G_VOL_MAX = 0.0;
|
|
|
|
|
static double G_VOL_STEP = 0.0;
|
|
|
|
|
static int G_DIGITS = 2;
|
|
|
|
|
static int G_STOPS_LEVEL = 0;
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
static int GetMTFDir()
|
|
|
|
|
{
|
|
|
|
|
if(!RequireMTFConfirm) return 0;
|
|
|
|
|
if(gEmaFastHandle==INVALID_HANDLE || gEmaSlowHandle==INVALID_HANDLE) return 0;
|
|
|
|
|
|
|
|
|
|
double fast[2], slow[2];
|
|
|
|
|
ArraySetAsSeries(fast, true);
|
|
|
|
|
ArraySetAsSeries(slow, true);
|
|
|
|
|
if(CopyBuffer(gEmaFastHandle, 0, 1, 1, fast) != 1) return 0;
|
|
|
|
|
if(CopyBuffer(gEmaSlowHandle, 0, 1, 1, slow) != 1) return 0;
|
|
|
|
|
if(fast[0] > slow[0]) return 1;
|
|
|
|
|
if(fast[0] < slow[0]) return -1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool HasOpenPosition(const string sym, const long magic)
|
|
|
|
|
{
|
|
|
|
|
for(int i=PositionsTotal()-1;i>=0;i--)
|
|
|
|
|
{
|
|
|
|
|
if(!PositionSelectByIndex(i)) continue;
|
|
|
|
|
string ps = PositionGetString(POSITION_SYMBOL);
|
|
|
|
|
if(ps != sym) continue;
|
|
|
|
|
if((long)PositionGetInteger(POSITION_MAGIC) != magic) continue;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static double NormalizeLots(const string sym, double lots)
|
|
|
|
|
{
|
2025-12-27 19:05:05 +00:00
|
|
|
// Use cached properties
|
|
|
|
|
lots = MathMax(G_VOL_MIN, MathMin(G_VOL_MAX, lots));
|
|
|
|
|
lots = MathFloor(lots/G_VOL_STEP) * G_VOL_STEP;
|
|
|
|
|
int volDigits = (int)MathRound(-MathLog10(G_VOL_STEP));
|
2025-12-26 07:26:47 +00:00
|
|
|
if(volDigits < 0) volDigits = 2;
|
|
|
|
|
if(volDigits > 8) volDigits = 8;
|
|
|
|
|
return NormalizeDouble(lots, volDigits);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 08:17:39 +00:00
|
|
|
static double LotsFromRisk(const string sym, const double riskPct, const double slPoints, const bool useEquity)
|
2025-12-26 07:26:47 +00:00
|
|
|
{
|
|
|
|
|
if(riskPct <= 0.0) return 0.0;
|
|
|
|
|
if(slPoints <= 0.0) return 0.0;
|
|
|
|
|
|
2025-12-26 08:17:39 +00:00
|
|
|
double base = (useEquity ? AccountInfoDouble(ACCOUNT_EQUITY) : AccountInfoDouble(ACCOUNT_BALANCE));
|
|
|
|
|
double riskMoney = base * (riskPct/100.0);
|
2025-12-26 07:26:47 +00:00
|
|
|
|
2025-12-27 19:05:05 +00:00
|
|
|
if(G_TICK_VALUE <= 0 || G_TICK_SIZE <= 0) return 0.0;
|
|
|
|
|
double valuePerPointPerLot = G_TICK_VALUE * (G_POINT / G_TICK_SIZE);
|
2025-12-26 07:26:47 +00:00
|
|
|
if(valuePerPointPerLot <= 0) return 0.0;
|
|
|
|
|
|
|
|
|
|
double lots = riskMoney / (slPoints * valuePerPointPerLot);
|
|
|
|
|
return lots;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 08:17:39 +00:00
|
|
|
static double NormalizePriceToTick(const string sym, double price)
|
|
|
|
|
{
|
2025-12-27 19:05:05 +00:00
|
|
|
// Use cached properties
|
|
|
|
|
double tick = (G_TICK_SIZE > 0.0 ? G_TICK_SIZE : G_POINT);
|
2025-12-26 08:17:39 +00:00
|
|
|
if(tick > 0.0) price = MathRound(price / tick) * tick;
|
2025-12-27 19:05:05 +00:00
|
|
|
return NormalizeDouble(price, G_DIGITS);
|
2025-12-26 08:17:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static double MinStopDistancePrice(const string sym)
|
|
|
|
|
{
|
2025-12-27 19:05:05 +00:00
|
|
|
// Use cached properties
|
|
|
|
|
return (G_STOPS_LEVEL > 0 ? G_STOPS_LEVEL * G_POINT : 0.0);
|
2025-12-26 08:17:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static double ClampLotsToMargin(const string sym, const ENUM_ORDER_TYPE type, double lots, const double price)
|
|
|
|
|
{
|
|
|
|
|
if(lots <= 0.0) return 0.0;
|
|
|
|
|
if(!RiskClampToFreeMargin) return lots;
|
|
|
|
|
|
|
|
|
|
double freeMargin = AccountInfoDouble(ACCOUNT_FREEMARGIN);
|
|
|
|
|
if(freeMargin <= 0.0) return 0.0;
|
|
|
|
|
|
|
|
|
|
double margin=0.0;
|
|
|
|
|
if(!OrderCalcMargin(type, sym, lots, price, margin)) return lots; // if broker doesn't provide calc, don't block
|
|
|
|
|
if(margin <= freeMargin) return lots;
|
|
|
|
|
|
|
|
|
|
// Estimate from 1-lot margin, then clamp down.
|
|
|
|
|
double margin1=0.0;
|
|
|
|
|
if(!OrderCalcMargin(type, sym, 1.0, price, margin1)) return lots;
|
|
|
|
|
if(margin1 <= 0.0) return lots;
|
|
|
|
|
|
|
|
|
|
double maxLots = (freeMargin / margin1) * 0.95; // small cushion
|
|
|
|
|
return MathMin(lots, maxLots);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
static void Notify(const string msg)
|
|
|
|
|
{
|
|
|
|
|
if(PopupAlerts) Alert(msg);
|
|
|
|
|
if(PushNotifications) SendNotification(msg);
|
2026-01-04 20:16:44 +00:00
|
|
|
SendWebRequest(msg);
|
2025-12-26 07:26:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int OnInit()
|
|
|
|
|
{
|
|
|
|
|
ENUM_TIMEFRAMES tf = (SignalTF==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : SignalTF);
|
|
|
|
|
|
|
|
|
|
gFractalsHandle = iFractals(_Symbol, tf);
|
|
|
|
|
if(gFractalsHandle == INVALID_HANDLE) return INIT_FAILED;
|
|
|
|
|
|
|
|
|
|
gAtrHandle = iATR(_Symbol, tf, ATRPeriod);
|
|
|
|
|
if(gAtrHandle == INVALID_HANDLE) return INIT_FAILED;
|
|
|
|
|
|
|
|
|
|
gEmaFastHandle = iMA(_Symbol, LowerTF, EMAFast, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
|
gEmaSlowHandle = iMA(_Symbol, LowerTF, EMASlow, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
|
|
|
|
|
|
gTrade.SetExpertMagicNumber(MagicNumber);
|
|
|
|
|
gTrade.SetDeviationInPoints(SlippagePoints);
|
2025-12-27 19:05:05 +00:00
|
|
|
|
|
|
|
|
// --- Cache symbol properties for performance
|
|
|
|
|
G_POINT = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
|
|
|
G_TICK_SIZE = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
|
|
|
G_TICK_VALUE = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOSS);
|
|
|
|
|
if(G_TICK_VALUE <= 0.0) G_TICK_VALUE = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
|
|
|
G_VOL_MIN = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
|
G_VOL_MAX = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
|
|
|
|
G_VOL_STEP = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
|
|
|
if(G_VOL_STEP <= 0) G_VOL_STEP = 0.01;
|
|
|
|
|
G_DIGITS = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
|
|
|
|
|
|
|
|
|
int stopsLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
|
|
|
|
|
int freezeLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
|
|
|
|
|
G_STOPS_LEVEL = MathMax(stopsLevel, freezeLevel);
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
return INIT_SUCCEEDED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnDeinit(const int reason)
|
|
|
|
|
{
|
|
|
|
|
if(gFractalsHandle != INVALID_HANDLE) IndicatorRelease(gFractalsHandle);
|
|
|
|
|
if(gAtrHandle != INVALID_HANDLE) IndicatorRelease(gAtrHandle);
|
|
|
|
|
if(gEmaFastHandle != INVALID_HANDLE) IndicatorRelease(gEmaFastHandle);
|
|
|
|
|
if(gEmaSlowHandle != INVALID_HANDLE) IndicatorRelease(gEmaSlowHandle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnTick()
|
|
|
|
|
{
|
feat(performance): Add new bar check to OnTick()
💡 What:
This commit introduces a performance optimization to the `OnTick()` function in the main Expert Advisor. It adds a check at the very beginning of the function to determine if a new bar has formed on the signal timeframe.
🎯 Why:
The `OnTick()` function is executed on every incoming price tick, which can happen multiple times per second. The core trading logic, however, is based on analyzing closed bars. Without this check, the EA was performing expensive calculations (like `CopyRates`, indicator calculations, and loop-based analysis) on every single tick, even when no new trading information was available. This was highly inefficient and consumed unnecessary CPU resources.
📊 Impact:
This change dramatically reduces the EA's computational overhead. The expensive logic inside `OnTick()` will now run only once per bar, instead of on every tick. This leads to a significant reduction in CPU usage, making the EA run much more efficiently, especially in volatile markets.
🔬 Measurement:
The improvement can be verified by observing the EA's execution frequency in the MetaTrader 5 "Experts" tab. Before this change, the log would show frequent processing. After this change, processing logs will only appear once at the beginning of each new bar on the selected timeframe.
2026-01-05 17:04:38 +00:00
|
|
|
// --- PERF: Check for new bar on SignalTF before running logic ---
|
|
|
|
|
// OnTick runs on price change, not bar change. This prevents expensive
|
|
|
|
|
// calculations on every tick, only running logic once per bar.
|
|
|
|
|
// Using iTime is a lightweight way to get the latest bar's timestamp.
|
2025-12-26 07:26:47 +00:00
|
|
|
ENUM_TIMEFRAMES tf = (SignalTF==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : SignalTF);
|
feat(performance): Add new bar check to OnTick()
💡 What:
This commit introduces a performance optimization to the `OnTick()` function in the main Expert Advisor. It adds a check at the very beginning of the function to determine if a new bar has formed on the signal timeframe.
🎯 Why:
The `OnTick()` function is executed on every incoming price tick, which can happen multiple times per second. The core trading logic, however, is based on analyzing closed bars. Without this check, the EA was performing expensive calculations (like `CopyRates`, indicator calculations, and loop-based analysis) on every single tick, even when no new trading information was available. This was highly inefficient and consumed unnecessary CPU resources.
📊 Impact:
This change dramatically reduces the EA's computational overhead. The expensive logic inside `OnTick()` will now run only once per bar, instead of on every tick. This leads to a significant reduction in CPU usage, making the EA run much more efficiently, especially in volatile markets.
🔬 Measurement:
The improvement can be verified by observing the EA's execution frequency in the MetaTrader 5 "Experts" tab. Before this change, the log would show frequent processing. After this change, processing logs will only appear once at the beginning of each new bar on the selected timeframe.
2026-01-05 17:04:38 +00:00
|
|
|
datetime latest_bar_time = iTime(_Symbol, tf, 0);
|
|
|
|
|
if(latest_bar_time == gLastTickBarTime)
|
|
|
|
|
{
|
|
|
|
|
return; // Not a new bar, exit early.
|
|
|
|
|
}
|
|
|
|
|
gLastTickBarTime = latest_bar_time; // Update for next tick.
|
|
|
|
|
|
2025-12-26 07:26:47 +00:00
|
|
|
|
|
|
|
|
// Pull recent bars from SignalTF
|
|
|
|
|
MqlRates rates[400];
|
|
|
|
|
ArraySetAsSeries(rates, true);
|
|
|
|
|
int needBars = MathMin(400, Bars(_Symbol, tf));
|
|
|
|
|
if(needBars < 100) return;
|
|
|
|
|
if(CopyRates(_Symbol, tf, 0, needBars, rates) < 100) return;
|
|
|
|
|
|
|
|
|
|
const int sigBar = (FireOnClose ? 1 : 0);
|
|
|
|
|
if(sigBar >= needBars-1) return;
|
|
|
|
|
|
|
|
|
|
// Run once per signal bar
|
|
|
|
|
datetime sigTime = rates[sigBar].time;
|
|
|
|
|
if(sigTime == gLastSignalBarTime) return;
|
|
|
|
|
gLastSignalBarTime = sigTime;
|
|
|
|
|
|
|
|
|
|
// Get fractals (for structure break)
|
|
|
|
|
int frNeed = MathMin(300, needBars);
|
|
|
|
|
double upFr[300], dnFr[300];
|
|
|
|
|
ArraySetAsSeries(upFr, true);
|
|
|
|
|
ArraySetAsSeries(dnFr, true);
|
|
|
|
|
if(CopyBuffer(gFractalsHandle, 0, 0, frNeed, upFr) <= 0) return;
|
|
|
|
|
if(CopyBuffer(gFractalsHandle, 1, 0, frNeed, dnFr) <= 0) return;
|
|
|
|
|
|
|
|
|
|
double lastSwingHigh = 0.0; datetime lastSwingHighT = 0;
|
|
|
|
|
double lastSwingLow = 0.0; datetime lastSwingLowT = 0;
|
|
|
|
|
for(int i=sigBar+2; i<frNeed; i++)
|
|
|
|
|
{
|
|
|
|
|
if(lastSwingHighT==0 && upFr[i] != 0.0) { lastSwingHigh = upFr[i]; lastSwingHighT = rates[i].time; }
|
|
|
|
|
if(lastSwingLowT==0 && dnFr[i] != 0.0) { lastSwingLow = dnFr[i]; lastSwingLowT = rates[i].time; }
|
|
|
|
|
if(lastSwingHighT!=0 && lastSwingLowT!=0) break;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 10:32:51 +00:00
|
|
|
// Donchian bounds (optimized)
|
|
|
|
|
// Using built-in iHighest/iLowest is faster than manual loops in MQL.
|
2025-12-26 08:17:39 +00:00
|
|
|
int donLookback = (DonchianLookback < 2 ? 2 : DonchianLookback);
|
2025-12-26 07:26:47 +00:00
|
|
|
int donStart = sigBar + 1;
|
2025-12-26 08:17:39 +00:00
|
|
|
int donCount = donLookback;
|
2025-12-26 07:26:47 +00:00
|
|
|
if(donStart + donCount >= needBars) return;
|
2025-12-26 10:32:51 +00:00
|
|
|
int highIndex = iHighest(_Symbol, tf, MODE_HIGH, donCount, donStart);
|
|
|
|
|
int lowIndex = iLowest(_Symbol, tf, MODE_LOW, donCount, donStart);
|
|
|
|
|
if(highIndex < 0 || lowIndex < 0) return; // Error case, data not ready
|
2025-12-30 10:33:03 +00:00
|
|
|
// PERF: Access price data directly from the copied 'rates' array.
|
|
|
|
|
// This avoids the function call overhead of iHigh/iLow, as the data is already in memory.
|
|
|
|
|
double donHigh = rates[highIndex].high;
|
|
|
|
|
double donLow = rates[lowIndex].low;
|
2025-12-26 07:26:47 +00:00
|
|
|
|
|
|
|
|
// Lower TF confirmation
|
|
|
|
|
int mtfDir = GetMTFDir();
|
|
|
|
|
bool mtfOkLong = (!RequireMTFConfirm) || (mtfDir == 1);
|
|
|
|
|
bool mtfOkShort = (!RequireMTFConfirm) || (mtfDir == -1);
|
|
|
|
|
|
|
|
|
|
// Signals
|
|
|
|
|
bool smcLong=false, smcShort=false, donLong=false, donShort=false;
|
|
|
|
|
double closeSig = rates[sigBar].close;
|
|
|
|
|
if(UseSMC)
|
|
|
|
|
{
|
|
|
|
|
if(lastSwingHighT!=0 && closeSig > lastSwingHigh) smcLong = true;
|
|
|
|
|
if(lastSwingLowT!=0 && closeSig < lastSwingLow) smcShort = true;
|
|
|
|
|
}
|
|
|
|
|
if(UseDonchianBreakout)
|
|
|
|
|
{
|
|
|
|
|
if(closeSig > donHigh) donLong = true;
|
|
|
|
|
if(closeSig < donLow) donShort = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool finalLong = (smcLong || donLong) && mtfOkLong;
|
|
|
|
|
bool finalShort = (smcShort || donShort) && mtfOkShort;
|
|
|
|
|
|
|
|
|
|
if(!finalLong && !finalShort) return;
|
|
|
|
|
|
|
|
|
|
// CHoCH / BOS label (informational)
|
|
|
|
|
string kind = "";
|
|
|
|
|
if(finalLong)
|
|
|
|
|
{
|
|
|
|
|
int breakDir = 1;
|
|
|
|
|
bool choch = (UseCHoCH && gTrendDir!=0 && breakDir != gTrendDir);
|
|
|
|
|
kind = (choch ? "CHoCH↑" : "BOS↑");
|
|
|
|
|
gTrendDir = breakDir;
|
|
|
|
|
}
|
|
|
|
|
if(finalShort)
|
|
|
|
|
{
|
|
|
|
|
int breakDir = -1;
|
|
|
|
|
bool choch = (UseCHoCH && gTrendDir!=0 && breakDir != gTrendDir);
|
|
|
|
|
kind = (choch ? "CHoCH↓" : "BOS↓");
|
|
|
|
|
gTrendDir = breakDir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string msg = StringFormat("%s %s %s | TF=%s | MTF=%s | SMC=%s DON=%s",
|
|
|
|
|
_Symbol,
|
|
|
|
|
(finalLong ? "LONG" : "SHORT"),
|
|
|
|
|
kind,
|
|
|
|
|
EnumToString(tf),
|
|
|
|
|
EnumToString(LowerTF),
|
|
|
|
|
(smcLong||smcShort ? "Y" : "N"),
|
|
|
|
|
(donLong||donShort ? "Y" : "N"));
|
|
|
|
|
Notify(msg);
|
|
|
|
|
|
|
|
|
|
if(!EnableTrading) return;
|
|
|
|
|
if(OnePositionPerSymbol && HasOpenPosition(_Symbol, MagicNumber)) return;
|
|
|
|
|
|
2025-12-26 08:17:39 +00:00
|
|
|
// ATR (always calculated; used for SL_ATR and fallbacks)
|
2025-12-26 07:26:47 +00:00
|
|
|
double atr[3];
|
|
|
|
|
ArraySetAsSeries(atr, true);
|
|
|
|
|
if(CopyBuffer(gAtrHandle, 0, sigBar, 1, atr) != 1) return;
|
|
|
|
|
double atrVal = atr[0];
|
|
|
|
|
if(atrVal <= 0) return;
|
|
|
|
|
|
2025-12-27 20:33:29 +00:00
|
|
|
// Cached point size (fallback to terminal-provided _Point)
|
|
|
|
|
double point = (G_POINT > 0.0 ? G_POINT : _Point);
|
2025-12-28 19:02:42 +00:00
|
|
|
// PERF: Use pre-defined Ask/Bid globals in OnTick to avoid function call overhead.
|
|
|
|
|
double ask = Ask;
|
|
|
|
|
double bid = Bid;
|
2025-12-26 07:26:47 +00:00
|
|
|
|
|
|
|
|
double entry = (finalLong ? ask : bid);
|
2025-12-26 08:17:39 +00:00
|
|
|
double sl = 0.0, tp = 0.0;
|
|
|
|
|
|
|
|
|
|
// --- Build SL
|
|
|
|
|
if(SLMode == SL_SWING)
|
|
|
|
|
{
|
|
|
|
|
// For a long breakout, protective SL typically goes below the last confirmed swing low.
|
|
|
|
|
// For a short breakout, SL goes above the last confirmed swing high.
|
2025-12-27 19:05:05 +00:00
|
|
|
double buf = SwingSLBufferPoints * G_POINT;
|
2025-12-26 08:17:39 +00:00
|
|
|
if(finalLong && lastSwingLowT != 0 && lastSwingLow > 0.0) sl = lastSwingLow - buf;
|
|
|
|
|
if(finalShort && lastSwingHighT != 0 && lastSwingHigh > 0.0) sl = lastSwingHigh + buf;
|
|
|
|
|
|
|
|
|
|
// Fallback if swing is missing/invalid for current entry.
|
|
|
|
|
if(finalLong && (sl <= 0.0 || sl >= entry)) sl = entry - (ATR_SL_Mult * atrVal);
|
|
|
|
|
if(finalShort && (sl <= 0.0 || sl <= entry)) sl = entry + (ATR_SL_Mult * atrVal);
|
|
|
|
|
}
|
|
|
|
|
else if(SLMode == SL_FIXED_POINTS)
|
|
|
|
|
{
|
|
|
|
|
double dist = MathMax(1, FixedSLPoints) * point;
|
|
|
|
|
sl = (finalLong ? entry - dist : entry + dist);
|
|
|
|
|
}
|
|
|
|
|
else // SL_ATR
|
|
|
|
|
{
|
|
|
|
|
sl = (finalLong ? entry - (ATR_SL_Mult * atrVal) : entry + (ATR_SL_Mult * atrVal));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Build TP
|
|
|
|
|
if(TPMode == TP_FIXED_POINTS)
|
|
|
|
|
{
|
|
|
|
|
double dist = MathMax(1, FixedTPPoints) * point;
|
|
|
|
|
tp = (finalLong ? entry + dist : entry - dist);
|
|
|
|
|
}
|
|
|
|
|
else if(TPMode == TP_DONCHIAN_WIDTH)
|
|
|
|
|
{
|
|
|
|
|
double width = MathAbs(donHigh - donLow);
|
|
|
|
|
if(width <= 0.0) width = ATR_SL_Mult * atrVal; // fallback
|
|
|
|
|
double dist = DonchianTP_Mult * width;
|
|
|
|
|
tp = (finalLong ? entry + dist : entry - dist);
|
|
|
|
|
}
|
|
|
|
|
else // TP_RR
|
|
|
|
|
{
|
|
|
|
|
double slDist = MathAbs(entry - sl);
|
|
|
|
|
tp = (finalLong ? entry + (RR * slDist) : entry - (RR * slDist));
|
|
|
|
|
}
|
2025-12-26 07:26:47 +00:00
|
|
|
|
|
|
|
|
// Respect broker minimum stop distance (in points)
|
2025-12-26 08:17:39 +00:00
|
|
|
double minStop = MinStopDistancePrice(_Symbol);
|
2025-12-26 07:26:47 +00:00
|
|
|
if(minStop > 0)
|
|
|
|
|
{
|
|
|
|
|
if(finalLong)
|
|
|
|
|
{
|
|
|
|
|
if(entry - sl < minStop) sl = entry - minStop;
|
|
|
|
|
if(tp - entry < minStop) tp = entry + minStop;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if(sl - entry < minStop) sl = entry + minStop;
|
|
|
|
|
if(entry - tp < minStop) tp = entry - minStop;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Respect tick size / digits
|
2025-12-26 08:17:39 +00:00
|
|
|
sl = NormalizePriceToTick(_Symbol, sl);
|
|
|
|
|
tp = NormalizePriceToTick(_Symbol, tp);
|
2025-12-26 07:26:47 +00:00
|
|
|
|
|
|
|
|
// Size
|
|
|
|
|
double slPoints = MathAbs(entry - sl) / point;
|
|
|
|
|
double lots = FixedLots;
|
|
|
|
|
if(RiskPercent > 0.0)
|
|
|
|
|
{
|
2025-12-26 08:17:39 +00:00
|
|
|
double riskLots = LotsFromRisk(_Symbol, RiskPercent, slPoints, RiskUseEquity);
|
2025-12-26 07:26:47 +00:00
|
|
|
if(riskLots > 0.0) lots = riskLots;
|
|
|
|
|
}
|
2025-12-26 08:17:39 +00:00
|
|
|
lots = NormalizeLots(_Symbol, ClampLotsToMargin(_Symbol, (finalLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), lots, entry));
|
2025-12-26 07:26:47 +00:00
|
|
|
if(lots <= 0.0) return;
|
|
|
|
|
|
|
|
|
|
bool ok = false;
|
|
|
|
|
if(finalLong)
|
|
|
|
|
ok = gTrade.Buy(lots, _Symbol, 0.0, sl, tp, "SMC_TB_MTF");
|
|
|
|
|
else
|
|
|
|
|
ok = gTrade.Sell(lots, _Symbol, 0.0, sl, tp, "SMC_TB_MTF");
|
|
|
|
|
|
|
|
|
|
if(!ok)
|
|
|
|
|
{
|
|
|
|
|
int err = GetLastError();
|
|
|
|
|
Notify(StringFormat("Order failed: %d", err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|