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 ---�
|