559 lines
16 KiB
MQL5
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);
|
|
}
|
|
}
|