1264 lines
76 KiB
Plaintext
1264 lines
76 KiB
Plaintext
// ★★★ 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 --- |