Files
forex-navigator/50-SingleLogic-LongShort/SingleLogic-LongShort.mq4
T

1264 lines
76 KiB
Plaintext
Raw Normal View History

2026-06-08 10:36:15 +02:00
// ★★★ EA_SingleLogic_LongShort ★★★
//---------------------------------------------
// (1) Input parameters
//---------------------------------------------
// --- Parameters for Magic_A
input double SARStep = 0.001; // SAR acceleration factor increment
input double SARMaxStep = 0.2; // Maximum SAR acceleration factor
input double TPPips = 90; // Take Profit in pips
input double SLPips = 70; // Stop Loss in pips
// Retry control parameters
extern int RetryCount = 3; // Number of retry attempts
extern int RetryIntervalMs = 1000; // Retry interval (milliseconds)
// --- Trailing stop settings
input bool UseTrailingStop = true; // true = enable trailing stop
input int TrailStepProfit = 30; // Trail activation profit (pips)
input int TrailShiftPips = 5; // Shift TP and SL by this many pips
// --- Fixed lot or compound lot selection
input bool UseCompoundLots = true; // true = compound, false = fixed lot
input double TargetMarginPercent = 5.0; // Target margin usage (% of balance)
extern double MarginLevelAlert = 250.0; // Margin level alert threshold (%)
// --- Trading hour restriction
input bool UseTradingHour = true; // true = restrict trading hours
input int TradingStartUTC = 7; // Trading start hour (UTC)
input int TradingEndUTC = 19; // Trading end hour (UTC, exclusive)
// --- EA status notification settings
extern bool NotifyEAStatus = true; // true = send status notifications
extern int StatusIntervalMin = 60; // Notification interval (minutes)
// --- Other settings
input double MaxSpreadPips = 3.0; // Maximum allowed spread (pips)
input int SlippagePips = 3; // Slippage (pips)
input double LotSize = 0.1; // Fixed lot size (for non-compound mode)
input int MaxOpenBuyPositions = 1; // Maximum BUY positions
input int MaxOpenSellPositions = 1; // Maximum SELL positions
#define BROKER_GMT_OFFSET_HOURS 2
//---------------------------------------------
// (2) Magic number
//---------------------------------------------
input int Magic_A = 67890;
//---------------------------------------------
// (3) Internal state variables
//---------------------------------------------
datetime prevTime_A = 0;
datetime lastEntryBarA = 0; // Bar time of the last entry (Magic_A)
datetime lastStatusSend = 0; // Last EA status notification time
bool EmergencyStop = false; // Emergency stop flag (internal)
bool EmergencyLatched = false; // Indicates if an emergency has ever occurred
// Exit latch counters
int buyExitTickCount = 0;
int sellExitTickCount = 0;
const int ExitTickThreshold = 5; // Required consecutive ticks
//---------------------------------------------
// (4) ENUM: Exit / Entry signals
//---------------------------------------------
enum SignalType
{
NO_SIGNAL = 0,
BUY_ENTRY,
BUY_EXIT,
SELL_ENTRY,
SELL_EXIT
};
//---------------------------------------------
// (5) Time utility functions (DST / UTC / trading hours)
//---------------------------------------------
// Assumes broker server time is UTC+2 (winter) / UTC+3 (summer)
// --- Daylight Saving Time detection (simplified, Northern Hemisphere)
bool IsDST(datetime t)
{
int year = TimeYear(t);
// Last Sunday of March
datetime marchLastSunday = StrToTime(StringFormat("%d.03.%d 02:00", year, 31));
while (TimeDayOfWeek(marchLastSunday) != 0)
marchLastSunday -= 86400;
// Last Sunday of October
datetime octLastSunday = StrToTime(StringFormat("%d.10.%d 03:00", year, 31));
while (TimeDayOfWeek(octLastSunday) != 0)
octLastSunday -= 86400;
return (t >= marchLastSunday && t < octLastSunday);
}
// --- Get current UTC hour (with DST adjustment)
int GetUTCHour()
{
datetime t = TimeCurrent();
int hour = TimeHour(t) - BROKER_GMT_OFFSET_HOURS;
if (IsDST(t))
hour -= 1; // Adjust for DST
if (hour < 0) hour += 24;
return hour;
}
// --- Trading hour check (can be enabled or disabled)
bool IsTradingHour()
{
if (!UseTradingHour) return true;
int utcHour = GetUTCHour();
return (utcHour >= TradingStartUTC && utcHour < TradingEndUTC);
}
//---------------------------------------------
// (6) New bar detection
//---------------------------------------------
bool IsNewBar(int timeframe, datetime &lastBarTime)
{
datetime currentBar = iTime(Symbol(), timeframe, 0);
if (currentBar != lastBarTime)
{
lastBarTime = currentBar;
return true;
}
return false;
}
//---------------------------------------------
// (7) Pip size calculation
//---------------------------------------------
double Pip()
{
return (Digits == 5 || Digits == 3) ? Point * 10 : Point;
}
//---------------------------------------------
// Convert pips to points (for slippage)
int PipToPoint(double pips)
{
return (int)MathRound(pips * Pip() / Point);
}
//---------------------------------------------
// (8) Lot size calculation: compound mode or fixed-lot mode
//---------------------------------------------
double CalculateLotSize()
{
if (!UseCompoundLots)
{
// Fixed-lot mode
double SlotStep = MarketInfo(_Symbol, MODE_LOTSTEP);
double SminLot = MarketInfo(_Symbol, MODE_MINLOT);
double SmaxLot = MarketInfo(_Symbol, MODE_MAXLOT);
double fixedLot = LotSize;
// Clamp to broker limits
double SadjustedLotSize = MathMax(SminLot, MathMin(fixedLot, SmaxLot));
// Align to lot step
SadjustedLotSize = MathFloor(SadjustedLotSize / SlotStep) * SlotStep;
return SadjustedLotSize;
}
// Compound mode (margin-based)
double accountBalance = AccountBalance();
double ClotStep = MarketInfo(_Symbol, MODE_LOTSTEP);
double CminLot = MarketInfo(_Symbol, MODE_MINLOT);
double CmaxLot = MarketInfo(_Symbol, MODE_MAXLOT);
double marginRequiredPerLot = MarketInfo(_Symbol, MODE_MARGINREQUIRED);
// -------------------------------------------------------
// Defensive: if margin information cannot be obtained due to broker specifications or symbol limitations,
// place an order using the symbol's minimum lot and log a warning.
// -------------------------------------------------------
if (marginRequiredPerLot <= 0.0)
{
PrintFormat(
"⚠ CalculateLotSize: invalid MODE_MARGINREQUIRED (%.5f). "
"Fallback to MINLOT=%.2f",
marginRequiredPerLot,
CminLot
);
return CminLot;
}
double targetMargin = accountBalance * TargetMarginPercent / 100.0;
double maxLotForMargin = targetMargin / marginRequiredPerLot;
double calculatedLotSize =
MathFloor(maxLotForMargin / ClotStep) * ClotStep;
double CadjustedLotSize =
MathMax(CminLot, MathMin(calculatedLotSize, CmaxLot));
return CadjustedLotSize;
}
//---------------------------------------------
// (9) Margin level check (anti-spam notification)
//---------------------------------------------
void CheckMarginLevel()
{
// Alert state (static to persist across ticks)
static bool alerted = false;
// No positions → no alert
if (OrdersTotal() == 0)
{
alerted = false;
return;
}
double margin = AccountMargin();
if (margin == 0)
{
alerted = false;
return;
}
double marginLevel = (AccountEquity() / margin) * 100.0;
// Alert trigger
if (marginLevel <= MarginLevelAlert && !alerted)
{
string timeStr = TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS);
string msg =
"[EA_SingleLogic_LongShort]\n"
"● Margin level below threshold ●\n"
"Symbol: " + Symbol() + "\n"
"Time: " + timeStr + "\n"
"Threshold: " + DoubleToString(MarginLevelAlert, 1) + "%\n"
"Current: " + DoubleToString(marginLevel, 1) + "%";
Print(msg);
SendNotification(msg);
alerted = true;
}
// Reset alert once margin recovers
if (marginLevel > MarginLevelAlert)
{
alerted = false;
}
}
//---------------------------------------------
// (10) EA runtime status notification
//---------------------------------------------
void CheckEAStatus()
{
if (!NotifyEAStatus) return;
if (TimeCurrent() - lastStatusSend >= StatusIntervalMin * 60)
{
string msg =
"[EA_SingleLogic_LongShort is running]\n" +
"Time: " + TimeToString(TimeCurrent(), TIME_SECONDS);
SendNotification(msg);
Print(msg);
lastStatusSend = TimeCurrent();
}
}
//---------------------------------------------
// (11) Count open positions by type
// type: OP_BUY or OP_SELL
//---------------------------------------------
int CountOrdersByTypeThisEA(int type)
{
int count = 0;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if (OrderSymbol() == Symbol() &&
OrderMagicNumber() == Magic_A &&
OrderType() == type)
{
count++;
}
}
}
return count;
}
//---------------------------------------------
// (12) Check if a new BUY order can be placed
// (emergency stop aware)
//---------------------------------------------
bool CanPlaceNewBuy()
{
if (EmergencyStop)
return false;
return CountOrdersByTypeThisEA(OP_BUY) < MaxOpenBuyPositions;
}
//---------------------------------------------
// (13) Check if a new SELL order can be placed
// (emergency stop aware)
//---------------------------------------------
bool CanPlaceNewSell()
{
if (EmergencyStop)
return false;
return CountOrdersByTypeThisEA(OP_SELL) < MaxOpenSellPositions;
}
//---------------------------------------------
// (14) Exit signal logic (BUY and SELL)
//---------------------------------------------
SignalType ExitSignalA()
{
double SAR0 = iSAR(_Symbol, 0, SARStep, SARMaxStep, 0);
double SAR1 = iSAR(_Symbol, 0, SARStep, SARMaxStep, 1);
double SAR2 = iSAR(_Symbol, 0, SARStep, SARMaxStep, 2);
// BUY exit conditions
if ((Low[1] > SAR1 && Close[0] <= SAR0) ||
(High[2] < SAR2 && High[1] < SAR1 && High[0] < SAR0))
{
buyExitTickCount++;
}
else
{
buyExitTickCount = 0;
}
// SELL exit conditions
if ((High[1] < SAR1 && Close[0] >= SAR0) ||
(Low[2] > SAR2 && Low[1] > SAR1 && Low[0] > SAR0))
{
sellExitTickCount++;
}
else
{
sellExitTickCount = 0;
}
// Return EXIT signal once latch threshold is reached
if (buyExitTickCount >= ExitTickThreshold) return BUY_EXIT;
if (sellExitTickCount >= ExitTickThreshold) return SELL_EXIT;
return NO_SIGNAL;
}
//---------------------------------------------
// (15) Entry signal logic (BUY and SELL)
//---------------------------------------------
SignalType EntrySignalA()
{
double SAR1 = iSAR(_Symbol, 0, SARStep, SARMaxStep, 1);
double SAR2 = iSAR(_Symbol, 0, SARStep, SARMaxStep, 2);
double SAR3 = iSAR(_Symbol, 0, SARStep, SARMaxStep, 3);
// BUY entry conditions
if ((High[2] < SAR2 && Close[1] >= SAR1) ||
(Low[3] > SAR3 && Low[2] > SAR2 && Low[1] > SAR1))
{
return BUY_ENTRY;
}
// SELL entry conditions
if ((Low[2] > SAR2 && Close[1] <= SAR1) ||
(High[3] < SAR3 && High[2] < SAR2 && High[1] < SAR1))
{
return SELL_ENTRY;
}
return NO_SIGNAL;
}
// ---------------------------------------------
// (16) RetryManager (single-task model)
// ---------------------------------------------
// Retry task types
enum RetryTaskType {
TASK_NONE = 0,
TASK_ENTRY,
TASK_EXIT,
TASK_MODIFY
};
// RetryTask structure
struct RetryTask {
RetryTaskType taskType;
int ticket; // Used for Exit / Modify
int orderType; // Entry: OP_BUY / OP_SELL
double lots;
double price; // Stored entry price (actual execution uses market price)
double sl;
double tp;
int slippage;
int retryLeft;
int lastAttemptMs;
};
// Global: single active retry task
RetryTask currentTask = { TASK_NONE };
// ---------------------------
// (17) EXIT-only queue (minimal implementation)
// ---------------------------
#define MAX_EXIT_QUEUE 20
RetryTask exitQueue[MAX_EXIT_QUEUE];
int exitQueueSize = 0;
bool PushExitQueue(const RetryTask &t)
{
if (exitQueueSize >= MAX_EXIT_QUEUE)
{
Print("❌ EXIT queue overflow");
return false;
}
exitQueue[exitQueueSize++] = t;
return true;
}
// EXIT queue helper functions
bool HasExitQueue()
{
return (exitQueueSize > 0);
}
RetryTask PopExitQueue()
{
RetryTask t = exitQueue[0];
for (int i = 1; i < exitQueueSize; i++)
exitQueue[i - 1] = exitQueue[i];
exitQueueSize--;
return t;
}
// ---------------------------
// (18) ResetCurrentTask
// ---------------------------
void ResetCurrentTask()
{
currentTask.taskType = TASK_NONE;
currentTask.ticket = 0;
currentTask.orderType = 0;
currentTask.lots = 0.0;
currentTask.price = 0.0;
currentTask.sl = 0.0;
currentTask.tp = 0.0;
currentTask.slippage = 0;
currentTask.retryLeft = 0;
currentTask.lastAttemptMs = 0;
}
// ---------------------------
// (19) RegisterRetryTask
// ---------------------------
bool RegisterRetryTask(const RetryTask &task)
{
if (currentTask.taskType != TASK_NONE)
{
PrintFormat(
"[RegisterRetryTask] Skip: another task active (exist=%d new=%d)",
currentTask.taskType,
task.taskType
);
return false;
}
currentTask = task;
currentTask.lastAttemptMs = 0; // Attempt immediately on next tick
PrintFormat(
"[RegisterRetryTask] Registered task type=%d ticket=%d",
currentTask.taskType,
currentTask.ticket
);
return true;
}
// ---------------------------
// (20) ProcessRetryTask
// ---------------------------
void ProcessRetryTask()
{
if (currentTask.taskType == TASK_NONE) return;
int now = GetTickCount();
// Wait until retry interval has elapsed
if (currentTask.lastAttemptMs != 0 &&
now - currentTask.lastAttemptMs < RetryIntervalMs)
{
return;
}
RefreshRates();
bool success = false;
int err = 0;
// ============================
// TASK_ENTRY
// ============================
if (currentTask.taskType == TASK_ENTRY)
{
// Always use the latest market price for entry
double execPrice = (currentTask.orderType == OP_BUY) ? Ask : Bid;
// Recalculate SL / TP based on execution price
double newSL, newTP;
if (currentTask.orderType == OP_BUY)
{
newSL = NormalizeDouble(execPrice - SLPips * Pip(), Digits);
newTP = NormalizeDouble(execPrice + TPPips * Pip(), Digits);
}
else
{
newSL = NormalizeDouble(execPrice + SLPips * Pip(), Digits);
newTP = NormalizeDouble(execPrice - TPPips * Pip(), Digits);
}
int ticket = OrderSend(
Symbol(),
currentTask.orderType,
currentTask.lots,
execPrice,
currentTask.slippage,
newSL,
newTP,
"Magic_A Entry",
Magic_A,
0,
clrOrange
);
if (ticket > 0)
{
success = true;
PrintFormat("✅ [Retry] Entry success: ticket=%d", ticket);
lastEntryBarA = iTime(Symbol(), PERIOD_CURRENT, 0);
}
else
{
err = GetLastError();
PrintFormat(
"❌ [Retry] Entry failed err=%d retryLeft=%d",
err,
currentTask.retryLeft
);
}
}
// ============================
// TASK_EXIT
// ============================
else if (currentTask.taskType == TASK_EXIT)
{
if (!OrderSelect(currentTask.ticket, SELECT_BY_TICKET))
{
// Order is already closed
success = true;
PrintFormat(
" [Retry] EXIT: ticket %d no longer exists -> treat as success",
currentTask.ticket
);
}
else
{
// Defensive handling for invalid lot size
if (currentTask.lots <= 0.0)
currentTask.lots = OrderLots();
int otype = OrderType();
double closePrice = (otype == OP_BUY) ? Bid : Ask;
bool closed = OrderClose(
currentTask.ticket,
currentTask.lots,
closePrice,
currentTask.slippage,
clrOrange
);
if (closed)
{
success = true;
PrintFormat(
"✅ [Retry] Exit success ticket=%d",
currentTask.ticket
);
}
else
{
err = GetLastError();
PrintFormat(
"❌ [Retry] Exit failed ticket=%d err=%d retryLeft=%d",
currentTask.ticket,
err,
currentTask.retryLeft
);
}
}
}
// ============================
// TASK_MODIFY
// ============================
else if (currentTask.taskType == TASK_MODIFY)
{
if (!OrderSelect(currentTask.ticket, SELECT_BY_TICKET))
{
// Order no longer exists
success = true;
PrintFormat(
" [Retry] MODIFY: ticket %d missing -> treat as success",
currentTask.ticket
);
}
else
{
double currOpen = OrderOpenPrice();
// Defensive correction for SL / TP values
double correctedSL =
(currentTask.sl <= 0) ? OrderStopLoss() : currentTask.sl;
double correctedTP =
(currentTask.tp <= 0) ? OrderTakeProfit() : currentTask.tp;
bool modified = OrderModify(
currentTask.ticket,
currOpen,
correctedSL,
correctedTP,
0,
clrAqua
);
if (modified)
{
success = true;
PrintFormat(
"✅ [Retry] Modify success ticket=%d",
currentTask.ticket
);
}
else
{
err = GetLastError();
PrintFormat(
"❌ [Retry] Modify failed ticket=%d err=%d retryLeft=%d",
currentTask.ticket,
err,
currentTask.retryLeft
);
}
}
}
// ============================
// Success handling
// ============================
if (success)
{
ResetCurrentTask();
return;
}
// ============================
// Error handling (retry or failure)
// ============================
bool transient = (
err == ERR_SERVER_BUSY ||
err == ERR_TRADE_CONTEXT_BUSY ||
err == ERR_PRICE_CHANGED ||
err == ERR_OFF_QUOTES ||
err == ERR_REQUOTE
);
if (transient && currentTask.retryLeft > 0)
{
currentTask.retryLeft--;
currentTask.lastAttemptMs = now;
PrintFormat(
"[Retry] transient error %d → retryLeft=%d",
err,
currentTask.retryLeft
);
return;
}
PrintFormat(
"[Retry] Task FAILED: type=%d ticket=%d err=%d",
currentTask.taskType,
currentTask.ticket,
err
);
ResetCurrentTask();
}
//---------------------------------------------
// (21) Exit handling (supports both BUY and SELL)
//---------------------------------------------
void HandleExitA(SignalType sig)
{
int slippagePoints = PipToPoint(SlippagePips);
// =======================
// BUY EXIT
// =======================
if (sig == BUY_EXIT)
{
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() == Magic_A &&
OrderSymbol() == _Symbol &&
OrderType() == OP_BUY)
{
RefreshRates(); // Fetch latest market prices
double BUYclosePrice = Bid;
RetryTask t;
t.taskType = TASK_EXIT;
t.ticket = OrderTicket();
t.lots = OrderLots();
t.price = BUYclosePrice; // ← added: reference close price
t.slippage = slippagePoints;
t.retryLeft = RetryCount;
if (PushExitQueue(t))
PrintFormat("📌 BUY EXIT queued (ticket=%d)", t.ticket);
}
}
return;
}
// =======================
// SELL EXIT
// =======================
if (sig == SELL_EXIT)
{
for (int m = OrdersTotal() - 1; m >= 0; m--)
{
if (!OrderSelect(m, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() == Magic_A &&
OrderSymbol() == _Symbol &&
OrderType() == OP_SELL)
{
RefreshRates(); // Fetch latest market prices
double SELLclosePrice = Ask;
RetryTask t;
t.taskType = TASK_EXIT;
t.ticket = OrderTicket();
t.lots = OrderLots();
t.price = SELLclosePrice; // ← added: reference close price
t.slippage = slippagePoints;
t.retryLeft = RetryCount;
if (PushExitQueue(t))
PrintFormat("📌 SELL EXIT queued (ticket=%d)", t.ticket);
}
}
}
}
//---------------------------------------------
// (22) Entry handling (supports both BUY and SELL)
//---------------------------------------------
void HandleEntryA(SignalType sig)
{
if (sig == NO_SIGNAL) return;
// 🚨 Block new entries while in emergency stop mode
if (EmergencyStop)
{
Print("🚫 EmergencyStop active. New entry is blocked: ", sig);
return;
}
datetime currentBarTime = iTime(Symbol(), PERIOD_CURRENT, 0);
// ▼ Second entry within the same bar → abnormal condition
if (lastEntryBarA == currentBarTime)
{
EmergencyCloseAll("Second entry detected within the same bar (abnormal)");
return;
}
if (!IsTradingHour()) return;
// Declare once and reuse
double orderLot;
// =======================
// BUY ENTRY
// =======================
if (sig == BUY_ENTRY)
{
double BUYspreadPips = (Ask - Bid) / Pip();
if (BUYspreadPips > MaxSpreadPips)
{
PrintFormat(
"❌ Spread too high (%.1f pips). Skip BUY entry.",
BUYspreadPips
);
return;
}
if (!CanPlaceNewBuy()) return;
double BUYprice = Ask;
double BUYsl = NormalizeDouble(BUYprice - SLPips * Pip(), Digits);
double BUYtp = NormalizeDouble(BUYprice + TPPips * Pip(), Digits);
int BUYslippagePoints = PipToPoint(SlippagePips);
// --- Retry-based entry (no Sleep, task registered to OnTick) ---
RefreshRates(); // Update prices
BUYprice = Ask; // Refresh execution price
orderLot = CalculateLotSize();
if (orderLot <= 0)
{
Print("[HandleEntryA] calculated lot <= 0. Fallback to LotSize param.");
orderLot = LotSize;
}
// === Register task to RetryManager ===
RetryTask t;
t.taskType = TASK_ENTRY;
t.orderType = OP_BUY;
t.lots = orderLot;
t.price = BUYprice;
t.sl = BUYsl;
t.tp = BUYtp;
t.slippage = BUYslippagePoints;
t.retryLeft = RetryCount;
if (RegisterRetryTask(t))
{
PrintFormat(
"📌 BUY ENTRY task registered. lot=%.2f sl=%.5f tp=%.5f",
orderLot,
BUYsl,
BUYtp
);
}
else
{
Print("⚠ BUY ENTRY task registration skipped (another task active)");
}
// BUY ends here (actual execution is handled in OnTick)
return;
}
// =======================
// SELL ENTRY
// =======================
if (sig == SELL_ENTRY)
{
double SELLspreadPips = (Ask - Bid) / Pip();
if (SELLspreadPips > MaxSpreadPips)
{
PrintFormat(
"❌ Spread too high (%.1f pips). Skip SELL entry.",
SELLspreadPips
);
return;
}
if (!CanPlaceNewSell()) return;
double SELLprice = Bid; // SELL uses Bid price
double SELLsl = NormalizeDouble(SELLprice + SLPips * Pip(), Digits);
double SELLtp = NormalizeDouble(SELLprice - TPPips * Pip(), Digits);
int SELLslippagePoints = PipToPoint(SlippagePips);
// --- Retry-based entry (no Sleep, task registered to OnTick) ---
RefreshRates();
SELLprice = Bid; // Refresh execution price
orderLot = CalculateLotSize();
if (orderLot <= 0)
{
Print("[HandleEntryA] calculated lot <= 0. Fallback to LotSize param.");
orderLot = LotSize;
}
// === Register task to RetryManager ===
RetryTask t;
t.taskType = TASK_ENTRY;
t.orderType = OP_SELL;
t.lots = orderLot;
t.price = SELLprice;
t.sl = SELLsl;
t.tp = SELLtp;
t.slippage = SELLslippagePoints;
t.retryLeft = RetryCount;
if (RegisterRetryTask(t))
{
PrintFormat(
"📌 SELL ENTRY task registered. lot=%.2f sl=%.5f tp=%.5f",
orderLot,
SELLsl,
SELLtp
);
}
else
{
Print("⚠ SELL ENTRY task registration skipped (another task active)");
}
// SELL ends here (actual execution is handled in OnTick)
return;
}
}
//---------------------------------------------
// (23) Step-based trailing stop
//---------------------------------------------
void ApplyTrailingStop()
{
if (!UseTrailingStop) return;
// ❗ Do not apply trailing while another retry task is active
if (currentTask.taskType != TASK_NONE) return;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != Magic_A) continue;
int type = OrderType();
if (type != OP_BUY && type != OP_SELL) continue;
int ticket = OrderTicket();
double openPrice = OrderOpenPrice();
double oldSL = OrderStopLoss();
double oldTP = OrderTakeProfit();
double currentPrice = (type == OP_BUY) ? Bid : Ask;
// Floating profit in pips
double profitPips = (currentPrice - openPrice) / Pip();
if (type == OP_SELL) profitPips = -profitPips;
// Minimum trailing condition not met
if (profitPips < TrailStepProfit) continue;
// ===============================
// 🔑 Calculate trailing step count
// ===============================
int stepCount = (int)(profitPips / TrailStepProfit);
double tgtSL, tgtTP, newSL, newTP;
if (type == OP_BUY)
{
tgtSL = openPrice + (stepCount * TrailShiftPips) * Pip();
tgtTP = openPrice + (stepCount * TrailShiftPips + TPPips) * Pip();
newSL = (oldSL < tgtSL) ? tgtSL : oldSL;
newTP = (oldTP < tgtTP) ? tgtTP : oldTP;
}
else
{
tgtSL = openPrice - (stepCount * TrailShiftPips) * Pip();
tgtTP = openPrice - (stepCount * TrailShiftPips + TPPips) * Pip();
newSL = (oldSL > tgtSL) ? tgtSL : oldSL;
newTP = (oldTP > tgtTP) ? tgtTP : oldTP;
}
newSL = NormalizeDouble(newSL, Digits);
newTP = NormalizeDouble(newTP, Digits);
// Skip update if the change is negligible
if (MathAbs(oldSL - newSL) <= Point * 0.5 &&
MathAbs(oldTP - newTP) <= Point * 0.5)
{
continue;
}
// ❗ Only one modify task can be registered at a time
if (currentTask.taskType != TASK_NONE) return;
// 🚀 Register MODIFY task to RetryManager (executed on OnTick)
RetryTask t;
t.taskType = TASK_MODIFY;
t.ticket = ticket;
t.sl = newSL;
t.tp = newTP;
t.retryLeft = RetryCount;
t.lastAttemptMs = 0;
if (RegisterRetryTask(t))
{
PrintFormat(
"🔧 StepTrailing MODIFY registered. ticket=%d step=%d sl=%.5f tp=%.5f",
ticket, stepCount, newSL, newTP
);
}
// ❗ Register only one task per tick
return;
}
}
//---------------------------------------------
// (24) Fail-safe abnormal state detection
//---------------------------------------------
void CheckForAbnormalState()
{
// 🚨 If emergency stop has already been latched, do nothing
// (reset only on EA restart)
if (EmergencyLatched)
return;
int openBuyCount = 0;
int openSellCount = 0;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != Magic_A) continue;
if (OrderSymbol() != Symbol()) continue;
int type = OrderType();
if (type == OP_BUY) openBuyCount++;
if (type == OP_SELL) openSellCount++;
}
// ▼ Check for maximum open position count violation
if (openBuyCount > MaxOpenBuyPositions)
{
EmergencyLatched = true; // ← latch ON
EmergencyCloseAll(
StringFormat(
"Abnormal BUY position count (%d > %d)",
openBuyCount,
MaxOpenBuyPositions
)
);
return;
}
if (openSellCount > MaxOpenSellPositions)
{
EmergencyLatched = true; // ← latch ON
EmergencyCloseAll(
StringFormat(
"Abnormal SELL position count (%d > %d)",
openSellCount,
MaxOpenSellPositions
)
);
return;
}
// Note:
// Duplicate entry within the same bar is not handled here.
// It is detected immediately before entry execution.
}
// ---------------------------------------------
// (25) Emergency close all positions and block new entries
// (EA continues running)
// ---------------------------------------------
void EmergencyCloseAll(string reasonMessage)
{
EmergencyStop = true; // Completely block new entries
Print("🚨 EmergencyCloseAll triggered. Reason = ", reasonMessage);
const int maxRetry = 200;
const int retryInterval = 200; // milliseconds (0.2s interval)
for (int attempt = 0; attempt < maxRetry; attempt++)
{
RefreshRates();
// ---- Process all orders matching this EA's Magic number ----
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != Magic_A) continue;
int type = OrderType();
// ▼ Market positions
if (type == OP_BUY || type == OP_SELL)
{
double price = (type == OP_BUY) ? Bid : Ask; // use latest price
bool closed = OrderClose(
OrderTicket(),
OrderLots(),
price,
5,
clrRed
);
if (!closed)
{
PrintFormat(
"⚠️ OrderClose failed ticket=%d error=%d (retry=%d)",
OrderTicket(), GetLastError(), attempt
);
}
else
{
PrintFormat(
"✅ Market position closed successfully ticket=%d",
OrderTicket()
);
}
}
// ▼ Pending orders
else if (type == OP_BUYLIMIT || type == OP_SELLLIMIT ||
type == OP_BUYSTOP || type == OP_SELLSTOP)
{
bool deleted = OrderDelete(OrderTicket());
if (!deleted)
{
PrintFormat(
"⚠️ OrderDelete failed ticket=%d error=%d (retry=%d)",
OrderTicket(), GetLastError(), attempt
);
}
else
{
PrintFormat(
"🗑️ Pending order deleted successfully ticket=%d",
OrderTicket()
);
}
}
}
// ---- Are all orders cleared? ----
if (CountOrdersOfThisEA() == 0)
{
string msg =
"[Emergency Close Completed]\n" +
"Symbol: " + Symbol() + "\n" +
"Time: " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "\n" +
"Reason: " + reasonMessage + "\n" +
"All positions have been closed/deleted.\n" +
"EA remains active in EmergencyStop mode (no new entries).";
SendNotification(msg);
Print(msg);
return;
}
if (attempt % 10 == 0)
{
PrintFormat(
"⏳ EmergencyClose retry %d/%d",
attempt, maxRetry
);
}
Sleep(retryInterval);
}
// ---- Orders still remain after maximum retries ----
string newmsg =
"[WARNING: Emergency Close Failed]\n" +
"Symbol: " + Symbol() + "\n" +
"Time: " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "\n" +
"Reason: " + reasonMessage + "\n" +
"Orders still remain after maximum retry attempts.\n" +
"EmergencyStop mode is maintained (entries remain blocked).";
SendNotification(newmsg);
Print(newmsg);
}
// ---------------------------------------------
// (26) Helper: count orders belonging to Magic_A
// (used to verify completion of emergency close)
// ---------------------------------------------
int CountOrdersOfThisEA()
{
int c = 0;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != Magic_A) continue;
c++;
}
return c;
}
//---------------------------------------------
// (27) Main OnTick processing
//---------------------------------------------
void OnTick()
{
RefreshRates();
// Check for abnormal/fail-safe conditions and margin alerts
CheckForAbnormalState();
CheckMarginLevel();
// =================================================
// ① Detect EXIT signals → push to EXIT queue (highest priority)
// =================================================
SignalType exitSig = ExitSignalA();
HandleExitA(exitSig); // ← does not register RetryTask directly; queued in EXIT queue
// =================================================
// ② If EXIT queue exists, flow the top task to RetryManager (highest priority)
// =================================================
if (currentTask.taskType == TASK_NONE && HasExitQueue())
{
RetryTask t = PopExitQueue();
RegisterRetryTask(t);
}
// =================================================
// ③ Trailing Stop (register MODIFY task)
// ※ Will naturally wait if an EXIT task is in progress
// =================================================
ApplyTrailingStop();
// =================================================
// ④ New bar entry (register ENTRY task)
// ※ Will naturally wait if an EXIT task is in progress
// =================================================
if (IsNewBar(PERIOD_CURRENT, prevTime_A))
{
SignalType entrySig = EntrySignalA();
HandleEntryA(entrySig);
}
// =================================================
// ⑤ Execute RetryManager
// =================================================
ProcessRetryTask();
// Send periodic EA status notification
CheckEAStatus();
}
// --- END ---