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
|
2026-01-05 11:37:02 +00:00
|
|
|
// Integrated with: https://github.com/Mouy-leng/-LengKundee-mql5.github.io.git
|
|
|
|
|
// Plugin: ZOLO-A6-9V-NUNA-
|
|
|
|
|
input string WebRequestURL = "https://soloist.ai/a6-9v";
|
2026-01-04 20:16:44 +00:00
|
|
|
|
|
|
|
|
// --- 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;
|
|
|
|
|
|
2026-01-08 17:03:42 +00:00
|
|
|
datetime gLastBarTime = 0;
|
2025-12-26 07:26:47 +00:00
|
|
|
int gTrendDir = 0; // 1 bullish, -1 bearish, 0 unknown (for CHoCH labelling)
|
|
|
|
|
|
2026-01-07 10:37:51 +00:00
|
|
|
// --- MTF Caching (performance)
|
|
|
|
|
datetime gLastMtfBarTime = 0;
|
|
|
|
|
int gCachedMtfDir = 0;
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-01-07 10:37:51 +00:00
|
|
|
// PERF: This function is cached. It only recalculates when a new bar appears on the LowerTF.
|
|
|
|
|
// This prevents expensive, redundant CopyBuffer calls on every tick of the primary chart.
|
2025-12-26 07:26:47 +00:00
|
|
|
static int GetMTFDir()
|
|
|
|
|
{
|
|
|
|
|
if(!RequireMTFConfirm) return 0;
|
|
|
|
|
|
2026-01-07 10:37:51 +00:00
|
|
|
// Check if a new COMPLETED bar has formed on the lower timeframe
|
|
|
|
|
datetime mtfTime[1]; // Array to hold one timestamp
|
|
|
|
|
ArraySetAsSeries(mtfTime, true);
|
|
|
|
|
// Read the timestamp of the last completed bar (index 1)
|
|
|
|
|
if(CopyTime(_Symbol, LowerTF, 1, 1, mtfTime) != 1) return 0; // On error, return safe default, not stale cache
|
|
|
|
|
if(mtfTime[0] == gLastMtfBarTime) return gCachedMtfDir; // No new bar, return cached
|
|
|
|
|
|
|
|
|
|
// --- New bar detected, recalculate ---
|
|
|
|
|
gLastMtfBarTime = mtfTime[0];
|
|
|
|
|
|
|
|
|
|
if(gEmaFastHandle==INVALID_HANDLE || gEmaSlowHandle==INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
gCachedMtfDir = 0; // Ensure cache is clean if handles are bad
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double fast[1], slow[1];
|
2025-12-26 07:26:47 +00:00
|
|
|
ArraySetAsSeries(fast, true);
|
|
|
|
|
ArraySetAsSeries(slow, true);
|
2026-01-07 10:37:51 +00:00
|
|
|
// Read from the last completed bar (index 1), consistent with the time check
|
|
|
|
|
if(CopyBuffer(gEmaFastHandle, 0, 1, 1, fast) != 1)
|
|
|
|
|
{
|
|
|
|
|
gCachedMtfDir = 0; // On error, reset cache to safe default
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if(CopyBuffer(gEmaSlowHandle, 0, 1, 1, slow) != 1)
|
|
|
|
|
{
|
|
|
|
|
gCachedMtfDir = 0; // On error, reset cache to safe default
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(fast[0] > slow[0]) gCachedMtfDir = 1;
|
|
|
|
|
else if(fast[0] < slow[0]) gCachedMtfDir = -1;
|
|
|
|
|
else gCachedMtfDir = 0;
|
|
|
|
|
|
|
|
|
|
return gCachedMtfDir;
|
2025-12-26 07:26:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
{
|
|
|
|
|
ENUM_TIMEFRAMES tf = (SignalTF==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : SignalTF);
|
|
|
|
|
|
2026-01-08 17:03:42 +00:00
|
|
|
// PERF: OnTick runs on every price change. A bar-based EA only needs to
|
|
|
|
|
// run its logic once when a new bar appears. This check provides an
|
|
|
|
|
// efficient early exit to prevent expensive calculations on every tick.
|
|
|
|
|
datetime time[1];
|
|
|
|
|
ArraySetAsSeries(time, true);
|
|
|
|
|
// Copy the timestamp of the most recent bar (index 0)
|
|
|
|
|
if(CopyTime(_Symbol, tf, 0, 1, time) != 1) return; // Error reading time
|
|
|
|
|
if(time[0] == gLastBarTime) return; // Not a new bar yet, exit
|
|
|
|
|
gLastBarTime = time[0]; // New bar detected, update and proceed
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|