PropGuardMT5/MQL5/Experts/PropGuardMT5/PropGuardMT5.mq5
2026-06-11 20:14:27 +03:00

559 lines
16 KiB
MQL5

#property copyright "Georgios Kagkas"
#property link "https://www.mql5.com"
#property version "1.000"
#property strict
#include <Trade/Trade.mqh>
enum DailyLossBaseMode
{
StartDayBalance = 0,
StartDayEquity = 1,
IntradayHighEquity = 2
};
enum ResetMode
{
BrokerMidnight = 0,
CustomServerTime = 1
};
enum BreachAction
{
AlertOnly = 0,
BlockNewTrades = 1,
CloseAllAndBlock = 2
};
enum LockedTradeAction
{
LockedAlertOnly = 0,
CloseNewTrades = 1
};
enum ScopeMode
{
AccountWide = 0,
CurrentSymbol = 1,
SymbolList = 2
};
input group "Prop Firm Limits"
input double InpDailyLossLimitPercent = 5.0;
input double InpDailyProfitTargetPercent = 0.0;
input double InpOverallDrawdownLimitPercent = 10.0;
input int InpMaxTradesPerDay = 10;
input double InpMaxLotsPerDay = 5.0;
input double InpOverallBaseBalance = 0.0; // 0 = capture current balance on first run
input group "Daily Reset"
input DailyLossBaseMode InpDailyLossBaseMode = StartDayEquity;
input ResetMode InpResetMode = BrokerMidnight;
input int InpResetHour = 0;
input int InpResetMinute = 0;
input group "Breach Behavior"
input BreachAction InpDailyBreachAction = BlockNewTrades;
input BreachAction InpOverallBreachAction = BlockNewTrades;
input LockedTradeAction InpLockedTradeAction = LockedAlertOnly;
input group "Scope"
input ScopeMode InpScopeMode = AccountWide;
input string InpSymbolsCSV = "EURUSD,XAUUSD";
input group "Execution and Alerts"
input int InpSlippagePoints = 20;
input bool InpEnablePopupAlerts = true;
input bool InpEnablePushNotifications = false;
CTrade trade;
string BUTTON_CLOSE_ALL = "PropGuard_CloseAll";
datetime g_resetStart = 0;
datetime g_nextReset = 0;
datetime g_lockTime = 0;
double g_startDayBalance = 0.0;
double g_startDayEquity = 0.0;
double g_intradayHighEquity = 0.0;
double g_overallBaseBalance = 0.0;
bool g_locked = false;
string g_lockReason = "None";
string Prefix()
{
return "PropGuardMT5." + IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN)) + ".";
}
string Key(const string name)
{
return Prefix() + name;
}
double LoadGlobalDouble(const string name, const double fallback)
{
const string key = Key(name);
if(GlobalVariableCheck(key))
return GlobalVariableGet(key);
GlobalVariableSet(key, fallback);
return fallback;
}
void SaveGlobalDouble(const string name, const double value)
{
GlobalVariableSet(Key(name), value);
}
string BaseModeText()
{
if(InpDailyLossBaseMode == StartDayBalance)
return "StartDayBalance";
if(InpDailyLossBaseMode == IntradayHighEquity)
return "IntradayHighEquity";
return "StartDayEquity";
}
string ResetModeText()
{
if(InpResetMode == CustomServerTime)
return "CustomServerTime";
return "BrokerMidnight";
}
string ScopeText()
{
if(InpScopeMode == CurrentSymbol)
return "CurrentSymbol";
if(InpScopeMode == SymbolList)
return "SymbolList";
return "AccountWide";
}
string BreachActionText(const BreachAction action)
{
if(action == AlertOnly)
return "AlertOnly";
if(action == CloseAllAndBlock)
return "CloseAllAndBlock";
return "BlockNewTrades";
}
bool SymbolInScope(const string symbol)
{
if(InpScopeMode == AccountWide)
return true;
if(InpScopeMode == CurrentSymbol)
return symbol == _Symbol;
string list = "," + InpSymbolsCSV + ",";
StringReplace(list, " ", "");
return StringFind(list, "," + symbol + ",") >= 0;
}
datetime ResetStartFor(const datetime serverNow)
{
MqlDateTime dt;
TimeToStruct(serverNow, dt);
int resetHour = 0;
int resetMinute = 0;
if(InpResetMode == CustomServerTime)
{
resetHour = MathMax(0, MathMin(23, InpResetHour));
resetMinute = MathMax(0, MathMin(59, InpResetMinute));
}
dt.hour = resetHour;
dt.min = resetMinute;
dt.sec = 0;
datetime resetStart = StructToTime(dt);
if(serverNow < resetStart)
resetStart -= 86400;
return resetStart;
}
datetime NextResetFor(const datetime serverNow)
{
return ResetStartFor(serverNow) + 86400;
}
void CaptureNewDailyBaseline(const datetime resetStart, const datetime serverNow)
{
g_resetStart = resetStart;
g_nextReset = resetStart + 86400;
g_startDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
g_startDayEquity = AccountInfoDouble(ACCOUNT_EQUITY);
g_intradayHighEquity = g_startDayEquity;
g_locked = false;
g_lockTime = 0;
g_lockReason = "None";
SaveGlobalDouble("resetStart", (double)g_resetStart);
SaveGlobalDouble("nextReset", (double)g_nextReset);
SaveGlobalDouble("startDayBalance", g_startDayBalance);
SaveGlobalDouble("startDayEquity", g_startDayEquity);
SaveGlobalDouble("intradayHighEquity", g_intradayHighEquity);
SaveGlobalDouble("locked", 0.0);
SaveGlobalDouble("lockTime", 0.0);
PrintFormat("PropGuard MT5: new daily baseline captured at %s. Balance %.2f, Equity %.2f",
TimeToString(serverNow, TIME_DATE | TIME_SECONDS),
g_startDayBalance,
g_startDayEquity);
}
void LoadState()
{
const datetime now = TimeTradeServer();
const datetime expectedResetStart = ResetStartFor(now);
g_resetStart = (datetime)LoadGlobalDouble("resetStart", (double)expectedResetStart);
g_nextReset = (datetime)LoadGlobalDouble("nextReset", (double)NextResetFor(now));
g_startDayBalance = LoadGlobalDouble("startDayBalance", AccountInfoDouble(ACCOUNT_BALANCE));
g_startDayEquity = LoadGlobalDouble("startDayEquity", AccountInfoDouble(ACCOUNT_EQUITY));
g_intradayHighEquity = LoadGlobalDouble("intradayHighEquity", AccountInfoDouble(ACCOUNT_EQUITY));
g_locked = LoadGlobalDouble("locked", 0.0) > 0.5;
g_lockTime = (datetime)LoadGlobalDouble("lockTime", 0.0);
if(InpOverallBaseBalance > 0.0)
g_overallBaseBalance = InpOverallBaseBalance;
else
g_overallBaseBalance = LoadGlobalDouble("overallBaseBalance", AccountInfoDouble(ACCOUNT_BALANCE));
SaveGlobalDouble("overallBaseBalance", g_overallBaseBalance);
if(g_resetStart != expectedResetStart)
CaptureNewDailyBaseline(expectedResetStart, now);
}
void PersistLock()
{
SaveGlobalDouble("locked", g_locked ? 1.0 : 0.0);
SaveGlobalDouble("lockTime", (double)g_lockTime);
}
double DailyLossBase()
{
if(InpDailyLossBaseMode == StartDayBalance)
return g_startDayBalance;
if(InpDailyLossBaseMode == IntradayHighEquity)
return g_intradayHighEquity;
return g_startDayEquity;
}
double DailyProfitBase()
{
if(InpDailyLossBaseMode == StartDayBalance)
return g_startDayBalance;
return g_startDayEquity;
}
double PercentOfBase(const double value, const double base)
{
if(base <= 0.0)
return 0.0;
return (value / base) * 100.0;
}
void Notify(const string message)
{
Print(message);
if(InpEnablePopupAlerts)
Alert(message);
if(InpEnablePushNotifications)
SendNotification(message);
}
void LockAccount(const string reason, const BreachAction action)
{
if(!g_locked)
{
g_locked = true;
g_lockTime = TimeTradeServer();
g_lockReason = reason;
PersistLock();
Notify("PropGuard MT5 LOCKED: " + reason + " | Action: " + BreachActionText(action));
}
if(action == CloseAllAndBlock)
CloseScopedPositions("Breach action: " + reason);
}
int CountScopedPositions()
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0 || !PositionSelectByTicket(ticket))
continue;
if(SymbolInScope(PositionGetString(POSITION_SYMBOL)))
count++;
}
return count;
}
void CloseScopedPositions(const string reason)
{
trade.SetDeviationInPoints(InpSlippagePoints);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0 || !PositionSelectByTicket(ticket))
continue;
const string symbol = PositionGetString(POSITION_SYMBOL);
if(!SymbolInScope(symbol))
continue;
if(trade.PositionClose(ticket))
PrintFormat("PropGuard MT5: closed ticket %I64u (%s). Reason: %s", ticket, symbol, reason);
else
PrintFormat("PropGuard MT5: failed to close ticket %I64u (%s). Retcode: %d",
ticket,
symbol,
trade.ResultRetcode());
}
}
void HandleLockedNewTrades()
{
if(!g_locked || InpLockedTradeAction != CloseNewTrades || g_lockTime <= 0)
return;
trade.SetDeviationInPoints(InpSlippagePoints);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0 || !PositionSelectByTicket(ticket))
continue;
const string symbol = PositionGetString(POSITION_SYMBOL);
if(!SymbolInScope(symbol))
continue;
const datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
if(openTime <= g_lockTime)
continue;
if(trade.PositionClose(ticket))
PrintFormat("PropGuard MT5: closed new locked-state trade ticket %I64u (%s).", ticket, symbol);
else
PrintFormat("PropGuard MT5: failed to close locked-state trade ticket %I64u (%s). Retcode: %d",
ticket,
symbol,
trade.ResultRetcode());
}
}
bool SelectTodayHistory()
{
return HistorySelect(g_resetStart, TimeTradeServer());
}
int TradesToday()
{
int count = 0;
if(!SelectTodayHistory())
return count;
const int total = HistoryDealsTotal();
for(int i = 0; i < total; i++)
{
const ulong deal = HistoryDealGetTicket(i);
if(deal == 0)
continue;
const string symbol = HistoryDealGetString(deal, DEAL_SYMBOL);
if(!SymbolInScope(symbol))
continue;
const long entry = HistoryDealGetInteger(deal, DEAL_ENTRY);
if(entry == DEAL_ENTRY_IN || entry == DEAL_ENTRY_INOUT)
count++;
}
return count;
}
double LotsToday()
{
double lots = 0.0;
if(!SelectTodayHistory())
return lots;
const int total = HistoryDealsTotal();
for(int i = 0; i < total; i++)
{
const ulong deal = HistoryDealGetTicket(i);
if(deal == 0)
continue;
const string symbol = HistoryDealGetString(deal, DEAL_SYMBOL);
if(!SymbolInScope(symbol))
continue;
const long entry = HistoryDealGetInteger(deal, DEAL_ENTRY);
if(entry == DEAL_ENTRY_IN || entry == DEAL_ENTRY_INOUT)
lots += HistoryDealGetDouble(deal, DEAL_VOLUME);
}
return lots;
}
void EvaluateRules()
{
const datetime now = TimeTradeServer();
const datetime expectedResetStart = ResetStartFor(now);
if(expectedResetStart != g_resetStart)
CaptureNewDailyBaseline(expectedResetStart, now);
const double balance = AccountInfoDouble(ACCOUNT_BALANCE);
const double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity > g_intradayHighEquity)
{
g_intradayHighEquity = equity;
SaveGlobalDouble("intradayHighEquity", g_intradayHighEquity);
}
const double dailyLossBase = DailyLossBase();
const double dailyProfitBase = DailyProfitBase();
const double dailyLossPercent = PercentOfBase(MathMax(0.0, dailyLossBase - equity), dailyLossBase);
const double dailyProfitPercent = PercentOfBase(equity - dailyProfitBase, dailyProfitBase);
const double overallDrawdownPercent = PercentOfBase(MathMax(0.0, g_overallBaseBalance - equity), g_overallBaseBalance);
const int trades = TradesToday();
const double lots = LotsToday();
if(InpDailyLossLimitPercent > 0.0 && dailyLossPercent >= InpDailyLossLimitPercent)
LockAccount(StringFormat("Daily loss %.2f%% >= %.2f%%", dailyLossPercent, InpDailyLossLimitPercent),
InpDailyBreachAction);
if(InpDailyProfitTargetPercent > 0.0 && dailyProfitPercent >= InpDailyProfitTargetPercent)
LockAccount(StringFormat("Daily profit target %.2f%% >= %.2f%%", dailyProfitPercent, InpDailyProfitTargetPercent),
InpDailyBreachAction);
if(InpOverallDrawdownLimitPercent > 0.0 && overallDrawdownPercent >= InpOverallDrawdownLimitPercent)
LockAccount(StringFormat("Overall drawdown %.2f%% >= %.2f%%", overallDrawdownPercent, InpOverallDrawdownLimitPercent),
InpOverallBreachAction);
if(InpMaxTradesPerDay > 0 && trades >= InpMaxTradesPerDay)
LockAccount(StringFormat("Trades today %d >= %d", trades, InpMaxTradesPerDay),
InpDailyBreachAction);
if(InpMaxLotsPerDay > 0.0 && lots >= InpMaxLotsPerDay)
LockAccount(StringFormat("Lots today %.2f >= %.2f", lots, InpMaxLotsPerDay),
InpDailyBreachAction);
HandleLockedNewTrades();
RenderDashboard(balance, equity, dailyLossPercent, dailyProfitPercent, overallDrawdownPercent, trades, lots);
}
void RenderDashboard(const double balance,
const double equity,
const double dailyLossPercent,
const double dailyProfitPercent,
const double overallDrawdownPercent,
const int trades,
const double lots)
{
const datetime now = TimeTradeServer();
const string lockState = g_locked ? "LOCKED" : "OK";
string text = "";
text += "PropGuard MT5 v1.000\n";
text += "State: " + lockState + "\n";
text += "Reason: " + g_lockReason + "\n";
text += "Server: " + TimeToString(now, TIME_DATE | TIME_SECONDS) + "\n";
text += "Next reset: " + TimeToString(g_nextReset, TIME_DATE | TIME_MINUTES) + "\n";
text += "Reset mode: " + ResetModeText() + "\n";
text += "Scope: " + ScopeText() + "\n";
text += "Daily base mode: " + BaseModeText() + "\n";
text += StringFormat("Balance: %.2f | Equity: %.2f\n", balance, equity);
text += StringFormat("Day balance: %.2f | Day equity: %.2f | Intraday high: %.2f\n",
g_startDayBalance,
g_startDayEquity,
g_intradayHighEquity);
text += StringFormat("Daily loss: %.2f%% / %.2f%%\n", dailyLossPercent, InpDailyLossLimitPercent);
text += StringFormat("Daily profit: %.2f%% / %.2f%%\n", dailyProfitPercent, InpDailyProfitTargetPercent);
text += StringFormat("Overall DD: %.2f%% / %.2f%%\n", overallDrawdownPercent, InpOverallDrawdownLimitPercent);
text += StringFormat("Trades today: %d / %d\n", trades, InpMaxTradesPerDay);
text += StringFormat("Lots today: %.2f / %.2f\n", lots, InpMaxLotsPerDay);
text += "Locked trade action: " + (InpLockedTradeAction == CloseNewTrades ? "CloseNewTrades" : "AlertOnly") + "\n";
Comment(text);
}
void CreateCloseAllButton()
{
if(ObjectFind(0, BUTTON_CLOSE_ALL) >= 0)
return;
ObjectCreate(0, BUTTON_CLOSE_ALL, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_XDISTANCE, 120);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_YDISTANCE, 20);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_XSIZE, 105);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_YSIZE, 26);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_BGCOLOR, clrFireBrick);
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_BORDER_COLOR, clrMaroon);
ObjectSetString(0, BUTTON_CLOSE_ALL, OBJPROP_TEXT, "Close All");
ObjectSetString(0, BUTTON_CLOSE_ALL, OBJPROP_TOOLTIP, "Close scoped positions");
}
int OnInit()
{
trade.SetDeviationInPoints(InpSlippagePoints);
LoadState();
CreateCloseAllButton();
EventSetTimer(1);
EvaluateRules();
Print("PropGuard MT5 initialized.");
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
ObjectDelete(0, BUTTON_CLOSE_ALL);
Comment("");
}
void OnTick()
{
EvaluateRules();
}
void OnTimer()
{
EvaluateRules();
}
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_OBJECT_CLICK && sparam == BUTTON_CLOSE_ALL)
{
CloseScopedPositions("Manual Close All button");
ObjectSetInteger(0, BUTTON_CLOSE_ALL, OBJPROP_STATE, false);
}
}