NUNA/Logs/Indicators/Downloads/EquiPeak Drawdown tracker sp.mq5
2026-01-06 05:44:21 +00:00

626 lines
46 KiB
MQL5

//+------------------------------------------------------------------+
//| Copyright 2025, Enrique Enguix |
//| https://www.mql5.com/es/users/envex |
//+------------------------------------------------------------------+
#property copyright "(c) 2025 Enrique Enguix"
#property link "https://www.mql5.com/es/users/envex"
#property version "1.02"
#property description "Rastrea el drawdown actual/histórico, con registro y notificaciones push."
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
#property strict
#include <Trade\PositionInfo.mqh>
//+------------------------------------------------------------------+
//| Objeto de información de posición |
//+------------------------------------------------------------------
CPositionInfo g_position;
//+------------------------------------------------------------------+
//| Enumeraciones |
//+------------------------------------------------------------------
enum EBalanceMode
{
REF_FIXED_BALANCE = 0, // Usar un balance fijo como referencia
REF_PEAK_BALANCE = 1 // Usar el balance pico como referencia (variable global)
};
enum EMaxDDUpdateMode
{
NO_UPDATE_MAX_DD = 0, // No actualizar el DD histórico máximo
UPDATE_MAX_DD_IF_BIGGER = 1 // Actualizar si el DD actual excede el histórico
};
//+------------------------------------------------------------------+
//| Enumeración para la selección de extensión de archivo |
//+------------------------------------------------------------------
enum SFileExtension
{
UseCSV = 0, // Usar formato CSV (compatible con Excel)
UseTXT = 1 // Usar formato TXT (texto plano)
};
//+------------------------------------------------------------------+
//| Parámetros de entrada |
//+------------------------------------------------------------------
input string MagicNumbers = "-1"; // Magics a monitorear (-1 rastrea todos)
input double InitialMaxDrawdown = 0.0; // DD Máximo Inicial (%)
input uint RefreshInterval = 60; // Frecuencia de actualización (segundos)
input EMaxDDUpdateMode UpdateMaxDrawdown = UPDATE_MAX_DD_IF_BIGGER; // Modo de actualización de MaxDD
input bool SendPushNotifications = true; // ¿Enviar notificaciones push?
//--- Modo de balance de referencia
input EBalanceMode ReferenceBalanceMode = REF_FIXED_BALANCE; // ¿Balance fijo o pico?
input double InpFixedBalance = 0.0; // Balance fijo (0 => balance actual)
//--- Estilo visual
input color CurrentDrawdownTextColor = clrGray; // Color para el texto del DD actual
input color MaxDrawdownTextColor = clrGray; // Color para el texto del DD máximo
input uint CurrentDrawdownFontSize = 25; // Tamaño de fuente (DD actual)
input uint MaxDrawdownFontSize = 25; // Tamaño de fuente (DD máximo)
input bool DisplayLabelsInBackground = true; // ¿Mostrar etiquetas detrás del gráfico?
//--- Posición y espacio para las etiquetas
input int LabelPosX = 200; // Posición X (pixeles)
input int LabelPosY = 20; // Posición Y (pixeles)
input uint LabelSpacing = 10; // Espaciado vertical
//--- Nivel de detalle del log
input bool PrintDetailedLogs = true; // ¿Imprimir detalles en el Journal?
//--- Nuevos inputs para registro en archivo
input bool EnableFileLog = true; // Crear un archivo independiente para registrar valores de DD actual
input SFileExtension InpFileExtension = UseTXT; // Extensión de archivo: CSV o TXT
//+------------------------------------------------------------------+
//| Variables globales (no son inputs del usuario) |
//+------------------------------------------------------------------
double g_ReferenceBalance = 0.0; // Balance de referencia usado en cálculos de DD
double g_MaxDrawdown = 0.0; // DD máximo histórico (%), se almacena globalmente
double g_PeakBalance = 0.0; // Balance pico (si se usa REF_PEAK_BALANCE)
// Nombres de las variables globales con sufijo único (evita conflictos entre instancias)
string g_PeakBalanceGVName;
string g_MaxDrawdownGVName;
// Guardaremos extensiones y otros parámetros de forma interna
uint g_RefreshIntervalVar = 60;
uint g_CurrentDrawdownFontSize = 35;
uint g_MaxDrawdownFontSize = 35;
string g_FileExtension = ".csv"; // Valor por defecto
// Nombres de objetos de texto con sufijo único (ChartID)
string g_currentDrawdownObjName;
string g_maxDrawdownObjName;
// Control de spam en modo NO_UPDATE_MAX_DD (1 notificación cada 60 min)
datetime g_LastPushTimeNOUPDATE = 0;
//+------------------------------------------------------------------+
//| OnInit: Inicialización |
//+------------------------------------------------------------------
int OnInit()
{
// Construir sufijos únicos para variables globales (usamos ChartID para distinguir instancias)
g_PeakBalanceGVName = "EquiPeakDT_PeakBalance_" + (string)ChartID();
g_MaxDrawdownGVName = "EquiPeakDT_MaxDD_" + (string)ChartID();
// Nombres de objetos de texto también únicos
g_currentDrawdownObjName = "current_drawdown_" + (string)ChartID();
g_maxDrawdownObjName = "max_drawdown_" + (string)ChartID();
//--- Validar RefreshInterval
if(RefreshInterval < 1)
{
g_RefreshIntervalVar = 10;
if(PrintDetailedLogs)
Print("[ADVERTENCIA] RefreshInterval < 1. Se usará 10 segundos como valor mínimo.");
}
else
if(RefreshInterval > 3600)
{
g_RefreshIntervalVar = 3600;
if(PrintDetailedLogs)
Print("[ADVERTENCIA] RefreshInterval > 3600. Se usará 3600 segundos (1 hora) como máximo.");
}
else
{
g_RefreshIntervalVar = RefreshInterval;
}
//--- Validar CurrentDrawdownFontSize
if(CurrentDrawdownFontSize < 1)
{
g_CurrentDrawdownFontSize = 12;
if(PrintDetailedLogs)
Print("[ADVERTENCIA] CurrentDrawdownFontSize <1. Se usará 12 como valor de respaldo.");
}
else
{
g_CurrentDrawdownFontSize = CurrentDrawdownFontSize;
}
//--- Validar MaxDrawdownFontSize
if(MaxDrawdownFontSize < 1)
{
g_MaxDrawdownFontSize = 12;
if(PrintDetailedLogs)
Print("[ADVERTENCIA] MaxDrawdownFontSize <1. Se usará 12 como valor de respaldo.");
}
else
{
g_MaxDrawdownFontSize = MaxDrawdownFontSize;
}
//--- Determinar extensión de archivo según enum
if(InpFileExtension == UseCSV)
g_FileExtension = ".csv";
else
g_FileExtension = ".txt";
//--- Establecer el temporizador con el intervalo validado
EventSetTimer(g_RefreshIntervalVar);
//--- Llamar OnTimer una vez para dibujar/logs inmediatamente
OnTimer();
//--- Configurar / leer el balance pico si se usa REF_PEAK_BALANCE
if(ReferenceBalanceMode == REF_PEAK_BALANCE)
{
if(GlobalVariableCheck(g_PeakBalanceGVName))
{
g_PeakBalance = GlobalVariableGet(g_PeakBalanceGVName);
if(PrintDetailedLogs)
PrintFormat("[INFO] Cargado balance pico desde variable global: %.2f", g_PeakBalance);
}
else
{
g_PeakBalance = AccountInfoDouble(ACCOUNT_BALANCE);
GlobalVariableSet(g_PeakBalanceGVName, g_PeakBalance);
if(PrintDetailedLogs)
PrintFormat("[INFO] No se encontró variable global. Creado balance pico inicial: %.2f", g_PeakBalance);
}
}
//--- Cargar o crear la variable global para el DD Máximo
if(GlobalVariableCheck(g_MaxDrawdownGVName))
{
g_MaxDrawdown = GlobalVariableGet(g_MaxDrawdownGVName);
if(PrintDetailedLogs)
PrintFormat("[INFO] Cargado DD máximo histórico desde variable global: %.2f%%", g_MaxDrawdown);
}
else
{
g_MaxDrawdown = InitialMaxDrawdown;
GlobalVariableSet(g_MaxDrawdownGVName, g_MaxDrawdown);
if(PrintDetailedLogs)
PrintFormat("[INFO] No se encontró variable global para DD máximo. Creado inicial con: %.2f%%", g_MaxDrawdown);
}
//--- Determinar balance de referencia
if(ReferenceBalanceMode == REF_FIXED_BALANCE)
{
if(InpFixedBalance <= 0.0)
{
double fallback = AccountInfoDouble(ACCOUNT_BALANCE);
g_ReferenceBalance = fallback;
if(PrintDetailedLogs)
PrintFormat("[ADVERTENCIA] InpFixedBalance <= 0. Se usará balance actual de la cuenta (%.2f).",
fallback);
}
else
{
g_ReferenceBalance = InpFixedBalance;
if(PrintDetailedLogs)
PrintFormat("[INFO] Usando Modo Balance Fijo. Referencia = %.2f", g_ReferenceBalance);
}
}
else // REF_PEAK_BALANCE
{
g_ReferenceBalance = g_PeakBalance;
if(PrintDetailedLogs)
PrintFormat("[INFO] Usando Modo Balance Pico. Referencia = %.2f", g_ReferenceBalance);
}
//--- Evitar división por cero
if(g_ReferenceBalance < 1e-8)
{
g_ReferenceBalance = 1e-8; // Valor mínimo
if(PrintDetailedLogs)
Print("[ERROR] El balance de referencia era 0 o inválido. Se fija en 1e-8 para evitar división por cero.");
}
//--- Mensaje final de inicialización
if(PrintDetailedLogs)
{
Print("[INFO] EquiPeakDrawdownTracker inicializado correctamente.");
PrintConfigurationSummary();
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| OnTimer: Se llama cada RefreshInterval segundos |
//+------------------------------------------------------------------
void OnTimer()
{
double realBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double floatingProfit = GetFloatingProfit();
double accountEquity = realBalance + floatingProfit;
//--- Si se usa PeakBalance, comprobar si aparece un nuevo pico (balance cerrado)
if(ReferenceBalanceMode == REF_PEAK_BALANCE)
{
if(realBalance > g_PeakBalance)
{
double oldPeak = g_PeakBalance;
g_PeakBalance = realBalance;
GlobalVariableSet(g_PeakBalanceGVName, g_PeakBalance);
g_ReferenceBalance = g_PeakBalance; // actualizar referencia
if(PrintDetailedLogs)
PrintFormat("[INFO] Detectado nuevo balance pico: %.2f (anterior pico era %.2f)",
g_PeakBalance, oldPeak);
}
}
//--- Calcular drawdown actual
double currentDrawdownPct = CalculateCurrentDrawdownPct(accountEquity, g_ReferenceBalance);
//--- Lógica para actualizar el DD máximo
if(UpdateMaxDrawdown == UPDATE_MAX_DD_IF_BIGGER)
{
// Si el DD actual es mayor que el DD histórico
if(currentDrawdownPct > g_MaxDrawdown)
{
double oldMax = g_MaxDrawdown;
g_MaxDrawdown = currentDrawdownPct;
GlobalVariableSet(g_MaxDrawdownGVName, g_MaxDrawdown);
if(PrintDetailedLogs)
PrintFormat("[INFO] Se ha alcanzado un nuevo DD máximo: %.2f%% (el anterior era %.2f%%)",
g_MaxDrawdown, oldMax);
// Enviar notificación push (solo si está configurado)
if(SendPushNotifications)
SendDetailedNotification(currentDrawdownPct, oldMax, true);
}
}
else // NO_UPDATE_MAX_DD
{
// Solo enviar notificación cada 60 minutos (3600 seg)
if(SendPushNotifications)
{
if((TimeCurrent() - g_LastPushTimeNOUPDATE) >= 3600)
{
SendDetailedNotification(currentDrawdownPct, g_MaxDrawdown, false);
g_LastPushTimeNOUPDATE = TimeCurrent();
}
}
}
//--- (1) Actualizar etiquetas en el gráfico
string currentDDText = StringFormat("Drawdown Actual: %.2f%%", currentDrawdownPct);
string maxDDText = StringFormat("Drawdown Histórico Máx: %.2f%%", g_MaxDrawdown);
// Etiqueta 1: drawdown actual
DrawLabel(g_currentDrawdownObjName,
currentDDText,
LabelPosX,
LabelPosY,
CurrentDrawdownTextColor,
g_CurrentDrawdownFontSize);
// Etiqueta 2: drawdown máximo
int secondLabelY = LabelPosY + (int)g_CurrentDrawdownFontSize + (int)LabelSpacing;
DrawLabel(g_maxDrawdownObjName,
maxDDText,
LabelPosX,
secondLabelY,
MaxDrawdownTextColor,
g_MaxDrawdownFontSize);
//--- (2) Registrar en archivo si está habilitado
if(EnableFileLog)
LogDrawdown(currentDrawdownPct);
}
//+------------------------------------------------------------------+
//| OnDeinit: Limpieza final |
//+------------------------------------------------------------------
void OnDeinit(const int reason)
{
EventKillTimer();
ObjectDelete(0, g_currentDrawdownObjName);
ObjectDelete(0, g_maxDrawdownObjName);
if(PrintDetailedLogs)
Print("[INFO] EquiPeakDrawdownTracker desinicializado.");
}
//+------------------------------------------------------------------+
//| OnCalculate: Función de indicador (no se usa para gráficos) |
//+------------------------------------------------------------------
int OnCalculate(const int rates_total,
const int prev_calculated,
const int begin,
const double &price[])
{
// Este indicador no dibuja ningún buffer.
return(rates_total);
}
//+------------------------------------------------------------------+
//| CalculateCurrentDrawdownPct: Retorna el DD flotante en % |
//+------------------------------------------------------------------
double CalculateCurrentDrawdownPct(const double equity, const double referenceBalance)
{
double eps = 1e-10;
double diff = referenceBalance - equity;
if(diff <= eps)
return 0.0; // no hay drawdown si la equity >= referenceBalance (o casi)
return (diff / referenceBalance) * 100.0;
}
//+------------------------------------------------------------------+
//| GetFloatingProfit: Retorna la ganancia/pérdida flotante total |
//| de los magics monitoreados |
//+------------------------------------------------------------------
double GetFloatingProfit()
{
double totalProfit = 0.0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(!g_position.SelectByIndex(i))
{
if(PrintDetailedLogs)
PrintFormat("[ADVERTENCIA] No se pudo seleccionar posición en índice %d", i);
continue;
}
long mNumber = g_position.Magic();
if(IsTrackedMagicNumber(mNumber))
{
// Profit() ya incluye swap y comisiones
totalProfit += g_position.Profit();
}
}
return totalProfit;
}
//+------------------------------------------------------------------+
//| IsTrackedMagicNumber: Compara el magic number con la lista |
//| (o -1) |
//+------------------------------------------------------------------
bool IsTrackedMagicNumber(long magicNumber)
{
string magicList[];
int count = StringSplit(MagicNumbers, ',', magicList);
bool trackAll = false;
//--- Primero comprobar si "-1" está en la lista
for(int i = 0; i < count; i++)
{
string magicStr = magicList[i];
StringReplace(magicStr, " ", ""); // quitar espacios
if(magicStr == "-1")
{
trackAll = true;
break;
}
}
if(trackAll)
return true;
//--- De lo contrario, comparar con cada valor
for(int i = 0; i < count; i++)
{
string magicStr = magicList[i];
StringReplace(magicStr, " ", "");
double parsed = StringToDouble(magicStr);
if(parsed == (double)magicNumber)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| SendDetailedNotification: Envía mensaje push con más detalles |
//+------------------------------------------------------------------
void SendDetailedNotification(double currentDD, double oldMaxDD, bool isNewRecord)
{
string server = AccountInfoString(ACCOUNT_SERVER);
long login = AccountInfoInteger(ACCOUNT_LOGIN);
string timeStr = TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS);
// Mensaje más completo
string msg = StringFormat(
"EquiPeak Drawdown Tracker v1.00\nBroker/Servidor: %s\nCuenta: %lld\nHora: %s\n"
"DD Actual: %.2f%%\nDD Histórico Máx: %.2f%%",
server,
login,
timeStr,
currentDD,
g_MaxDrawdown
);
if(isNewRecord)
msg += StringFormat("\n[RECORD] ¡Nuevo DD Máx alcanzado! (el anterior era %.2f%%)", oldMaxDD);
bool sent = SendNotification(msg);
if(!sent && PrintDetailedLogs)
{
PrintFormat("[ADVERTENCIA] No se pudo enviar notificación push: %s", msg);
}
}
//+------------------------------------------------------------------+
//| PrintConfigurationSummary: Muestra la configuración elegida |
//+------------------------------------------------------------------
void PrintConfigurationSummary()
{
if(!PrintDetailedLogs)
return;
Print("===== Resumen de Configuración EquiPeak Drawdown Tracker =====");
// Magic numbers
PrintFormat(" > MagicNumbers a rastrear: %s", MagicNumbers);
// Modo de Balance
if(ReferenceBalanceMode == REF_FIXED_BALANCE)
PrintFormat(" > Modo de Balance: FIJO, balance de referencia = %.2f", g_ReferenceBalance);
else
PrintFormat(" > Modo de Balance: PICO (pico cargado = %.2f)", g_PeakBalance);
// Modo de actualización del DD
if(UpdateMaxDrawdown == UPDATE_MAX_DD_IF_BIGGER)
Print(" > Modo de Actualización del DD Máx: ACTUALIZAR SI ES MAYOR");
else
Print(" > Modo de Actualización del DD Máx: NO ACTUALIZAR (con notificaciones limitadas)");
// DD máximo actual
PrintFormat(" > DD Máx almacenado actualmente = %.2f%%", g_MaxDrawdown);
// Intervalo de refresco
PrintFormat(" > Intervalo de refresco (segundos): %u", g_RefreshIntervalVar);
// Notificaciones push
PrintFormat(" > Notificaciones Push: %s", SendPushNotifications ? "HABILITADAS" : "DESHABILITADAS");
// Etiquetas
PrintFormat(" > Tamaños de fuente (DD/MaxDD): %u / %u", g_CurrentDrawdownFontSize, g_MaxDrawdownFontSize);
PrintFormat(" > Posiciones de etiqueta: X=%d, Y=%d, Espaciado=%u", LabelPosX, LabelPosY, LabelSpacing);
// Registro en archivo
PrintFormat(" > Registro en Archivo: %s", EnableFileLog ? "HABILITADO" : "DESHABILITADO");
if(EnableFileLog)
{
string tf = PeriodToStringLowerCase(_Period);
string fileName = "DD_" + _Symbol + "_" + tf + "_Magic_" + MagicNumbers + g_FileExtension;
PrintFormat(" > Extensión de archivo: %s", g_FileExtension);
PrintFormat(" > Ruta de archivo (aprox.): Common/Files/%s", fileName);
}
Print("==============================================================");
}
//+------------------------------------------------------------------+
//| LogDrawdown: Añade info de DD en un archivo (CSV o TXT) |
//+------------------------------------------------------------------
void LogDrawdown(double currentDrawdown)
{
string symbol = _Symbol;
string tfString = PeriodToStringLowerCase(_Period);
string fileName = "DD_" + symbol + "_" + tfString + "_Magic_" + MagicNumbers + g_FileExtension;
int flags = FILE_READ | FILE_WRITE | FILE_COMMON;
if(g_FileExtension == ".csv")
flags |= FILE_CSV;
int handle = FileOpen(fileName, flags);
if(handle == INVALID_HANDLE)
{
Print("[ERROR] No se pudo abrir/crear el archivo de log: ", fileName);
return;
}
FileSeek(handle, 0, SEEK_END);
string timeStr = TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS);
// Obtener valores de Balance y Equity
double realBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double equity = realBalance + GetFloatingProfit();
// Escribir en el archivo: se agregarán Balance y Equity a la línea de log
FileWrite(handle, timeStr, MagicNumbers, DoubleToString(currentDrawdown, 2),
DoubleToString(realBalance, 2), DoubleToString(equity, 2));
FileClose(handle);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------
string PeriodToStringLowerCase(ENUM_TIMEFRAMES period)
{
switch(period)
{
case PERIOD_M1:
return "m1";
case PERIOD_M2:
return "m2";
case PERIOD_M3:
return "m3";
case PERIOD_M4:
return "m4";
case PERIOD_M5:
return "m5";
case PERIOD_M6:
return "m6";
case PERIOD_M10:
return "m10";
case PERIOD_M12:
return "m12";
case PERIOD_M15:
return "m15";
case PERIOD_M20:
return "m20";
case PERIOD_M30:
return "m30";
case PERIOD_H1:
return "h1";
case PERIOD_H2:
return "h2";
case PERIOD_H3:
return "h3";
case PERIOD_H4:
return "h4";
case PERIOD_H6:
return "h6";
case PERIOD_H8:
return "h8";
case PERIOD_H12:
return "h12";
case PERIOD_D1:
return "d1";
case PERIOD_W1:
return "w1";
case PERIOD_MN1:
return "mn1";
default:
return "unknown";
}
}
//+------------------------------------------------------------------+
//| DrawLabel: Crea/actualiza una etiqueta en el gráfico |
//+------------------------------------------------------------------
void DrawLabel(string name,
string text,
int x,
int y,
color clr,
uint fontSize)
{
if(ObjectFind(0, name) == -1)
{
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, name, OBJPROP_BACK, DisplayLabelsInBackground);
}
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
ObjectSetString(0, name, OBJPROP_TEXT, text);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
}
//+------------------------------------------------------------------+