MQL5-Google-Onedrive/mt5/MQL5/Experts/SMC_TrendBreakout_MTF_EA.mq5
2026-02-27 02:36:33 +07:00

644 lines
23 KiB
MQL5

//+------------------------------------------------------------------+
//| SMC_TrendBreakout_MTF_EA.mq5 |
//| EA: SMC (BOS/CHoCH) + Donchian breakout + MTF confirmation |
//| Alerts / Push notifications + optional auto-trading |
//| AI Integration: Gemini API for signal confirmation |
//| Updated by: Jules (AI Assistant) for LengKundee |
//+------------------------------------------------------------------+
#property strict
#property copyright "LengKundee"
#property link "https://forge.mql5.io/LengKundee/mql5.git"
#property version "1.22"
#include <Trade/Trade.mqh>
#include <ZoloBridge.mqh>
#include <AiAssistant.mqh>
#include <ManagePositions.mqh>
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
};
input group "Core"
input bool EnableTrading = true; // if false: alerts only
input long MagicNumber = 81001;
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"
input ENUM_SL_MODE SLMode = SL_ATR;
input ENUM_TP_MODE TPMode = TP_RR;
input double FixedLots = 0.10; // used when RiskPercent=0
input double RiskPercent = 1.0; // if >0: position size from SL distance
input bool RiskUseEquity = true; // recommended
input bool RiskClampToFreeMargin = true; // reduce lots if not enough margin
input int ATRPeriod = 14;
input double ATR_SL_Mult = 2.0;
input int SwingSLBufferPoints = 20; // extra points beyond swing (SL_SWING)
input int FixedSLPoints = 500; // SL_FIXED_POINTS
input double RR = 2.0;
input int FixedTPPoints = 1000; // TP_FIXED_POINTS
input double DonchianTP_Mult = 1.0; // TP_DONCHIAN_WIDTH
input int SlippagePoints = 30;
input group "Scalping / Management"
input bool UseBreakEven = true;
input double BE_Trigger_Pips = 10.0;
input double BE_Plus_Pips = 2.0;
input bool UseTrailing = true;
input double Trail_Start_Pips = 15.0;
input double Trail_Step_Pips = 5.0;
input group "AI Filter"
input bool UseGeminiFilter = false; // Enable AI confirmation (formerly UseGeminiFilter)
input ENUM_AI_PROVIDER AiProvider = PROVIDER_GEMINI; // Select AI Provider
input group "Gemini Settings"
input string GeminiApiKey = ""; // Paste your Gemini API Key here
input string GeminiModel = "gemini-1.5-flash"; // e.g., gemini-1.5-flash
input group "Jules Settings"
input string JulesApiKey = ""; // Paste your Jules API Key here
input string JulesApiUrl = ""; // Enter Jules API URL (e.g. https://api.jules.ai/v1/completion)
input string JulesModel = "jules-v1"; // Jules Model ID
input group "AI Context"
input string PerplexityUrl = "https://www.perplexity.ai/finance/EURZ"; // Bridge to Perplexity (Manual/Context)
input int RSIPeriod = 14; // Period for RSI context in AI prompt
input group "Notifications"
input bool PopupAlerts = true;
input bool PushNotifications = true;
input group "ZOLO Integration"
input bool EnableWebRequest = false; // off by default for safety
input string WebRequestURL = ""; // set to your bridge URL (and allow it in MT5)
input string ZoloEncryptionKey = ""; // Leave empty for no encryption
CTrade gTrade;
CPositionManager gPosManager;
int gFractalsHandle = INVALID_HANDLE;
int gAtrHandle = INVALID_HANDLE;
int gEmaFastHandle = INVALID_HANDLE;
int gEmaSlowHandle = INVALID_HANDLE;
int gRsiHandle = INVALID_HANDLE;
datetime gLastSignalBarTime = 0;
int gTrendDir = 0; // 1 bullish, -1 bearish, 0 unknown (for CHoCH labelling)
// PERF: Cached signal timeframe.
ENUM_TIMEFRAMES gSignalTf = PERIOD_CURRENT;
// PERF: Cached validated Donchian lookback.
int gDonchianLookback = 20;
// --- 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 double G_INV_VOL_STEP = 0.0;
static int G_DIGITS = 2;
static int G_STOPS_LEVEL = 0;
static double G_MIN_STOP_PRICE = 0.0;
// --- Cached Risk & Signal Constants (performance)
static double g_riskMultiplier = 0.0;
static double g_lotValuePerUnit = 0.0;
static double g_swingSLBuffer = 0.0;
static double g_fixedSL = 0.0;
static double g_fixedTP = 0.0;
// --- Cached MTF direction (performance)
// The lower-TF EMA direction only needs to be checked once per new bar on that TF.
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.
// OPTIMIZATION: Use iTime for a faster check than CopyTime.
datetime mtf_time = iTime(_Symbol, LowerTF, 0);
if(mtf_time == 0) return 0; // if data not ready, return neutral
if(mtf_time == g_mtfDir_lastCheckTime) return g_mtfDir_cachedValue; // return cached value
g_mtfDir_lastCheckTime = mtf_time;
double fast[2], slow[2];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
// Using CopyBuffer on bar 1 (last completed bar) to avoid repainting.
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;
}
// --- Cached ATR value (performance)
// The ATR only needs to be calculated once per OnTick event, and only if needed.
static double g_atr_cachedValue = 0.0;
static datetime g_atr_cacheTime = 0; // The bar time this cache is valid for.
// PERF: Lazy-loads the ATR value for the current signal bar.
// This avoids an expensive CopyBuffer call if the SL/TP modes do not require ATR.
static double GetATR(const int signalBar, const datetime signalBarTime)
{
// If we already calculated ATR for this specific bar, return the cached value.
if(g_atr_cacheTime == signalBarTime && g_atr_cachedValue > 0.0)
{
return g_atr_cachedValue;
}
// Reset cache if the bar time is different.
if(g_atr_cacheTime != signalBarTime)
{
g_atr_cachedValue = 0.0;
g_atr_cacheTime = signalBarTime;
}
if(gAtrHandle == INVALID_HANDLE) return 0.0;
double atr[1];
if(CopyBuffer(gAtrHandle, 0, signalBar, 1, atr) != 1) return 0.0;
if(atr[0] <= 0.0) return 0.0;
g_atr_cachedValue = atr[0]; // Cache the valid ATR.
return g_atr_cachedValue;
}
static bool HasOpenPosition(const string sym, const long magic)
{
for(int i=PositionsTotal()-1;i>=0;i--)
{
if(!PositionSelectByIndex(i)) continue;
string positionSymbol = PositionGetString(POSITION_SYMBOL);
if(positionSymbol != sym) continue;
if((long)PositionGetInteger(POSITION_MAGIC) != magic) continue;
return true;
}
return false;
}
static double NormalizeLots(const string sym, double lots)
{
// ⚡ Bolt: Using cached inverse for faster multiplication instead of division
lots = MathMax(G_VOL_MIN, MathMin(G_VOL_MAX, lots));
lots = MathFloor(lots * G_INV_VOL_STEP) * G_VOL_STEP;
int volumeDecimalPlaces = (int)MathRound(-MathLog10(G_VOL_STEP));
if(volumeDecimalPlaces < 0) volumeDecimalPlaces = 2;
if(volumeDecimalPlaces > 8) volumeDecimalPlaces = 8;
return NormalizeDouble(lots, volumeDecimalPlaces);
}
static double LotsFromRisk(const string sym, const double riskPct, const double slPoints, const bool useEquity)
{
// ⚡ Bolt: Use pre-calculated multipliers to avoid redundant divisions
if(riskPct <= 0.0 || slPoints <= 0.0 || g_lotValuePerUnit <= 0.0) return 0.0;
double accountRiskBase = (useEquity ? AccountInfoDouble(ACCOUNT_EQUITY) : AccountInfoDouble(ACCOUNT_BALANCE));
double riskMoney = accountRiskBase * g_riskMultiplier;
return riskMoney / (slPoints * g_lotValuePerUnit);
}
static double NormalizePriceToTick(const string sym, double price)
{
// Use cached properties
double tick = (G_TICK_SIZE > 0.0 ? G_TICK_SIZE : G_POINT);
if(tick > 0.0) price = MathRound(price / tick) * tick;
return NormalizeDouble(price, G_DIGITS);
}
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 marginPerLot=0.0;
if(!OrderCalcMargin(type, sym, 1.0, price, marginPerLot)) return lots;
if(marginPerLot <= 0.0) return lots;
double maxLots = (freeMargin / marginPerLot) * 0.95; // small cushion
return MathMin(lots, maxLots);
}
static void Notify(const string msg)
{
if(PopupAlerts) Alert(msg);
if(PushNotifications) SendNotification(msg);
}
int OnInit()
{
// PERF: Calculate and cache the signal timeframe once.
gSignalTf = (SignalTF==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)_Period : SignalTF);
gFractalsHandle = iFractals(_Symbol, gSignalTf);
if(gFractalsHandle == INVALID_HANDLE) return INIT_FAILED;
gAtrHandle = iATR(_Symbol, gSignalTf, ATRPeriod);
if(gAtrHandle == INVALID_HANDLE) return INIT_FAILED;
// PERF: Validate and cache Donchian lookback once.
gDonchianLookback = (DonchianLookback < 2 ? 2 : DonchianLookback);
gEmaFastHandle = iMA(_Symbol, LowerTF, EMAFast, 0, MODE_EMA, PRICE_CLOSE);
gEmaSlowHandle = iMA(_Symbol, LowerTF, EMASlow, 0, MODE_EMA, PRICE_CLOSE);
gRsiHandle = iRSI(_Symbol, gSignalTf, RSIPeriod, PRICE_CLOSE);
if(gRsiHandle == INVALID_HANDLE) return INIT_FAILED;
gTrade.SetExpertMagicNumber(MagicNumber);
gTrade.SetDeviationInPoints(SlippagePoints);
gPosManager.Init(&gTrade);
// --- Cache symbol properties for performance
G_POINT = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(G_POINT <= 0.0) G_POINT = _Point; // Fallback
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_INV_VOL_STEP = 1.0 / G_VOL_STEP;
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);
G_MIN_STOP_PRICE = (G_STOPS_LEVEL > 0 ? G_STOPS_LEVEL * G_POINT : 0.0);
// ⚡ Bolt: Pre-calculate constants for hot-path optimization
g_riskMultiplier = RiskPercent / 100.0;
g_lotValuePerUnit = (G_TICK_SIZE > 0 ? G_TICK_VALUE * (G_POINT / G_TICK_SIZE) : 0.0);
g_swingSLBuffer = SwingSLBufferPoints * G_POINT;
g_fixedSL = MathMax(1, FixedSLPoints) * G_POINT;
g_fixedTP = MathMax(1, FixedTPPoints) * G_POINT;
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);
if(gRsiHandle != INVALID_HANDLE) IndicatorRelease(gRsiHandle);
}
void OnTick()
{
// Manage open positions (Trailing Stop / Break Even) every tick
if(EnableTrading)
{
gPosManager.Manage(MagicNumber, _Symbol, UseBreakEven, BE_Trigger_Pips, BE_Plus_Pips, UseTrailing, Trail_Start_Pips, Trail_Step_Pips);
}
// PERF: Early exit if a new bar hasn't formed on the signal timeframe.
// This is a critical optimization that prevents expensive calls (like CopyRates)
// from running on every single price tick within the same bar.
const int sigBar = (FireOnClose ? 1 : 0);
// OPTIMIZATION: Use iTime for a faster check than CopyTime.
datetime sigTime = iTime(_Symbol, gSignalTf, sigBar);
if(sigTime == 0) return; // Data not ready
if(sigTime == gLastSignalBarTime) return; // Not a new signal bar, exit early.
// Now that we've passed all checks, we can commit to this bar time.
gLastSignalBarTime = sigTime;
// --- Data Loading & Primary Signals ---
// PERF: Defer expensive data loading. Only load full history if needed.
double lastSwingHigh = 0.0; datetime lastSwingHighT = 0;
double lastSwingLow = 0.0; datetime lastSwingLowT = 0;
double closeSig = 0.0;
// PERF: Lazy Calculation - only search for swings if needed for SMC or SL.
if(UseSMC || SLMode == SL_SWING)
{
// PERF: Array allocation is deferred to this block to avoid overhead on the lighter path.
// OPTIMIZATION: Use simple datetime array instead of full MqlRates struct to save memory/bandwidth.
datetime times[400];
ArraySetAsSeries(times, true);
// This path requires a deep history for fractal/swing analysis.
int needBars = MathMin(400, Bars(_Symbol, gSignalTf));
if(needBars < 100) return;
// OPTIMIZATION: CopyTime is faster and uses less memory than CopyRates (8 bytes vs 60 bytes per bar).
if(CopyTime(_Symbol, gSignalTf, 0, needBars, times) < 100) return;
if(sigBar >= needBars-1) return;
// OPTIMIZATION: Use iClose for single price access instead of accessing heavy struct array.
closeSig = iClose(_Symbol, gSignalTf, sigBar); // Get close directly.
// Get fractals (for structure break)
int fractalBarsNeeded = MathMin(300, needBars);
double upwardFractals[300], downwardFractals[300];
ArraySetAsSeries(upwardFractals, true);
ArraySetAsSeries(downwardFractals, true);
if(CopyBuffer(gFractalsHandle, 0, 0, fractalBarsNeeded, upwardFractals) <= 0) return;
if(CopyBuffer(gFractalsHandle, 1, 0, fractalBarsNeeded, downwardFractals) <= 0) return;
for(int i=sigBar+2; i<fractalBarsNeeded; i++)
{
if(lastSwingHighT==0 && upwardFractals[i] != 0.0) { lastSwingHigh = upwardFractals[i]; lastSwingHighT = times[i]; }
if(lastSwingLowT==0 && downwardFractals[i] != 0.0) { lastSwingLow = downwardFractals[i]; lastSwingLowT = times[i]; }
if(lastSwingHighT!=0 && lastSwingLowT!=0) break;
}
}
else
{
// This path is much lighter.
// PERF: Use the lightweight iClose() instead of heavy CopyRates() just to get a single price.
closeSig = iClose(_Symbol, gSignalTf, sigBar);
if(closeSig <= 0.0) return; // Abort if price is invalid.
}
// --- Donchian Channel (using native CopyHigh/CopyLow for performance) ---
// OPTIMIZATION: Using CopyHigh/CopyLow with ArrayMaximum/ArrayMinimum is the standard
// and most efficient way in MQL5 to calculate a range high/low without a custom indicator.
int donStart = sigBar + 1;
double highs[], lows[];
if(CopyHigh(_Symbol, gSignalTf, donStart, gDonchianLookback, highs) != gDonchianLookback) return;
if(CopyLow(_Symbol, gSignalTf, donStart, gDonchianLookback, lows) != gDonchianLookback) return;
double donHigh = highs[ArrayMaximum(highs)];
double donLow = lows[ArrayMinimum(lows)];
if(donHigh <= 0 || donLow <= 0) return; // Data not ready or invalid
// --- Primary Signals (without MTF confirmation yet) ---
bool smcLong=false, smcShort=false, donLong=false, donShort=false;
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;
}
// PERF: Early exit if no primary signal exists. This avoids the GetMTFDir()
// call (which performs a CopyTime) on the vast majority of bars.
if(!(smcLong || donLong || smcShort || donShort)) return;
// --- Lower TF confirmation (only after a primary signal) ---
int mtfDir = GetMTFDir();
bool mtfOkLong = (!RequireMTFConfirm) || (mtfDir == 1);
bool mtfOkShort = (!RequireMTFConfirm) || (mtfDir == -1);
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(gSignalTf),
EnumToString(LowerTF),
(smcLong||smcShort ? "Y" : "N"),
(donLong||donShort ? "Y" : "N"));
Notify(msg);
if(EnableWebRequest) SendSignalToBridge(msg, EnableWebRequest, WebRequestURL, ZoloEncryptionKey);
if(!EnableTrading) return;
if(OnePositionPerSymbol && HasOpenPosition(_Symbol, MagicNumber)) return;
// PERF: G_POINT is now guaranteed to be valid from OnInit.
double point = G_POINT;
// PERF: Use SymbolInfoTick to get both Ask and Bid efficiently.
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick)) return;
double currentAsk = tick.ask;
double currentBid = tick.bid;
double entry = (finalLong ? currentAsk : currentBid);
double sl = 0.0, tp = 0.0;
// --- AI Filter ---
if(UseGeminiFilter)
{
double rsiValue = 50.0;
double atrValue = 0.0;
// Get RSI
double rsiBuffer[1];
if(CopyBuffer(gRsiHandle, 0, sigBar, 1, rsiBuffer) == 1) rsiValue = rsiBuffer[0];
// Get ATR (using existing helper logic or direct copy)
atrValue = GetATR(sigBar, sigTime);
string prompt = Ai_ConstructPrompt(_Symbol, (finalLong ? "BUY" : "SELL"), entry, gTrendDir, rsiValue, atrValue, PerplexityUrl);
bool aiConfirmed = false;
if (AiProvider == PROVIDER_GEMINI)
{
aiConfirmed = Ai_AskGemini(GeminiApiKey, GeminiModel, prompt);
}
else if (AiProvider == PROVIDER_JULES)
{
aiConfirmed = Ai_AskJules(JulesApiKey, JulesModel, prompt, JulesApiUrl);
}
if(!aiConfirmed)
{
Print("AI Filter (" + EnumToString(AiProvider) + ") rejected the trade or request failed.");
return;
}
Print("AI Filter (" + EnumToString(AiProvider) + ") confirmed the trade.");
}
// --- 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.
// ⚡ Bolt: Use pre-calculated buffer
if(finalLong && lastSwingLowT != 0 && lastSwingLow > 0.0) sl = lastSwingLow - g_swingSLBuffer;
if(finalShort && lastSwingHighT != 0 && lastSwingHigh > 0.0) sl = lastSwingHigh + g_swingSLBuffer;
// Fallback if swing is missing/invalid for current entry.
if((finalLong && (sl <= 0.0 || sl >= entry)) || (finalShort && (sl <= 0.0 || sl <= entry)))
{
// PERF: ATR is lazy-loaded only for this fallback case.
double atrValue = GetATR(sigBar, sigTime);
if(atrValue > 0.0)
{
if(finalLong) sl = entry - (ATR_SL_Mult * atrValue);
else sl = entry + (ATR_SL_Mult * atrValue);
}
}
}
else if(SLMode == SL_FIXED_POINTS)
{
// ⚡ Bolt: Use pre-calculated fixed SL
sl = (finalLong ? entry - g_fixedSL : entry + g_fixedSL);
}
else // SL_ATR
{
// PERF: ATR is lazy-loaded only when this SL mode is active.
double atrValue = GetATR(sigBar, sigTime);
if(atrValue > 0.0)
{
sl = (finalLong ? entry - (ATR_SL_Mult * atrValue) : entry + (ATR_SL_Mult * atrValue));
}
}
// CRITICAL: If SL is 0 after this block, it means a required calculation
// (like GetATR) failed. Abort to prevent placing a trade with no stop loss.
if(sl == 0.0)
{
// Optionally notify the user about the failure.
// Notify(StringFormat("SL calculation failed for %s.", _Symbol));
return;
}
// --- Build TP
if(TPMode == TP_FIXED_POINTS)
{
// ⚡ Bolt: Use pre-calculated fixed TP
tp = (finalLong ? entry + g_fixedTP : entry - g_fixedTP);
}
else if(TPMode == TP_DONCHIAN_WIDTH)
{
double width = MathAbs(donHigh - donLow);
if(width <= 0.0)
{
// PERF: ATR is lazy-loaded only for this fallback case.
double atrValue = GetATR(sigBar, sigTime);
if(atrValue > 0.0) width = ATR_SL_Mult * atrValue; // 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));
}
// CRITICAL: If TP is 0, it means a calculation failed. Abort.
if(tp == 0.0) return;
// Respect broker minimum stop distance (in points)
if(G_MIN_STOP_PRICE > 0)
{
if(finalLong)
{
if(entry - sl < G_MIN_STOP_PRICE) sl = entry - G_MIN_STOP_PRICE;
if(tp - entry < G_MIN_STOP_PRICE) tp = entry + G_MIN_STOP_PRICE;
}
else
{
if(sl - entry < G_MIN_STOP_PRICE) sl = entry + G_MIN_STOP_PRICE;
if(entry - tp < G_MIN_STOP_PRICE) tp = entry - G_MIN_STOP_PRICE;
}
}
// Respect tick size / digits
sl = NormalizePriceToTick(_Symbol, sl);
tp = NormalizePriceToTick(_Symbol, tp);
// Size
double slPoints = MathAbs(entry - sl) / point;
double lots = FixedLots;
if(RiskPercent > 0.0)
{
double riskLots = LotsFromRisk(_Symbol, RiskPercent, slPoints, RiskUseEquity);
if(riskLots > 0.0) lots = riskLots;
}
lots = NormalizeLots(_Symbol, ClampLotsToMargin(_Symbol, (finalLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), lots, entry));
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));
}
}
void OnTimer()
{
// Optional: Position management can be done in OnTimer or OnTick.
// Doing it in OnTick is fine if ticks are frequent, which they are in scalping.
}