Estas son las funciones mas importantes de un Expert Advisor (EA) en MQL5
Aprende la estructura exacta de un Expert Advisor en MQL5: OnInit, OnTick, OnDeinit y OnTester con código funcional, gráficas y ejercicios prácticos para MetaTrade
En el Episodio 1 viste un EA abrir su primera operación. Funcionó, pero probablemente no entendiste cada línea. Eso cambia aquí. Vamos a diseccionar un Expert Advisor completo: sus cuatro funciones de evento, los tipos de variables, el sistema de handles para indicadores y el flujo exacto de ejecución tick a tick.
No es una clase de programación abstracta. Cada concepto va acompañado del código que lo implementa y del gráfico que lo explica. Al final de este artículo tendrás el mapa mental completo de cómo MT5 corre tu robot — y eso cambia la forma en que escribes código.
01 La estructura general de un EA
Un Expert Advisor en MQL5 es un archivo .mq5 que MetaEditor compila a bytecode .ex5. Ese bytecode lo ejecuta la máquina virtual de MT5 en respuesta a eventos: un nuevo tick, la inicialización del EA, el cierre de la plataforma, etc. No hay un bucle while(true) — el modelo es reactivo, no imperativo.
INIT_SUCCEEDED, el EA no arranca.IndicatorRelease(), archivos abiertos, objetos gráficos.double que puede usarse como criterio de optimización personalizado. Lo veremos en el Episodio 10.02 OnInit() — El constructor del robot
OnInit() es la primera función que ejecuta MT5 cuando adjuntas el EA a un gráfico. Se llama exactamente una vez. Su trabajo es preparar todo lo que el EA necesitará durante su vida: crear handles de indicadores, validar parámetros, calcular constantes. Si algo falla aquí, retornas INIT_FAILED y el EA no arranca — lo cual es exactamente lo que quieres.
OnInit(). Todo lo que va en OnTick() se repite miles de veces — ponlo solo si es imprescindible ahí.
Copia este código en MetaEditor, compila y adjunta al gráfico EURUSD M5. Observa el panel de Expertos: verás los mensajes de inicialización. Luego cambia InpMAPeriod a 0 y recompila — el EA rechazará el parámetro inválido.
#property strict // ── Parámetros configurables desde MT5 ──────────────────────────── input int InpMAPeriod = 20; // Período MA input double InpLots = 0.10; // Lotaje // ── Variable global para el handle ──────────────────────────────── int g_maHandle; int OnInit() { // 1. Validar parámetros ANTES de crear handles if(InpMAPeriod < 2) { Alert("ERROR: InpMAPeriod debe ser >= 2. Valor recibido: ", InpMAPeriod); return(INIT_PARAMETERS_INCORRECT); // EA no arranca } if(InpLots <= 0) { Alert("ERROR: Lotaje debe ser positivo"); return(INIT_PARAMETERS_INCORRECT); } // 2. Crear el handle del indicador g_maHandle = iMA(_Symbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); // 3. Verificar que el handle es válido if(g_maHandle == INVALID_HANDLE) { Print("ERROR al crear handle MA. Código: ", GetLastError()); return(INIT_FAILED); } // 4. Log de confirmación — visible en Herramientas > Diario de Expertos Print("EA iniciado correctamente en ", _Symbol, " ", EnumToString(_Period)); Print("MA(20) handle: ", g_maHandle); return(INIT_SUCCEEDED); // EA arranca }
03 OnTick() — El corazón que late con el mercado
OnTick() se ejecuta cada vez que llega un nuevo precio del broker. En USD/JPY durante la sesión de Londres puede llamarse más de 2,000 veces por minuto. Es donde vive toda la lógica de tu estrategia: leer precios, evaluar indicadores, decidir si entrar o salir. Lo que pongas aquí se repite constantemente — cada milisegundo cuenta.
IsNewBar(). Sin ese control, estarás evaluando señales en barras incompletas.
Este es el patrón que usarás en el 90% de tus EAs. Copia, compila y observa el panel de Expertos: verás un mensaje cada vez que se cierre una vela con el valor actual de la MA.
// Variable global para detectar nueva vela datetime g_lastBarTime = 0; void OnTick() { // ── PASO 1: Detectar nueva vela ────────────────────────────── datetime currentBarTime = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE); bool isNewBar = (currentBarTime != g_lastBarTime); if(!isNewBar) return; // Salir si no es nueva vela — sin hacer nada g_lastBarTime = currentBarTime; // ── PASO 2: Esperar suficientes barras ─────────────────────── if(Bars(_Symbol, _Period) < InpMAPeriod + 10) { Print("Esperando datos suficientes..."); return; } // ── PASO 3: Leer el indicador ──────────────────────────────── double ma[2]; ArraySetAsSeries(ma, true); int copied = CopyBuffer(g_maHandle, 0, 0, 2, ma); if(copied < 2) { Print("ERROR: CopyBuffer devolvió solo ", copied, " valores"); return; } // ── PASO 4: Leer precio actual ─────────────────────────────── double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // ── PASO 5: Evaluar señal ──────────────────────────────────── bool precioSobreMA = (bid > ma[0]); bool precioSobreMAAnterior = (bid > ma[1]); Print("Nueva vela | MA: ", DoubleToString(ma[0],_Digits), " | Precio ", (precioSobreMA ? "SOBRE" : "BAJO"), " la MA"); }
04 OnDeinit() — El cierre limpio
OnDeinit() es el espejo de OnInit(): se ejecuta una sola vez al final de la vida del EA. Su propósito es liberar todos los recursos que creaste en la inicialización. El parámetro reason te dice exactamente por qué se está cerrando el EA, lo que permite tomar decisiones distintas según el motivo.
Adjunta el EA, luego quítalo del gráfico (clic derecho → Eliminar). Observa el Diario de Expertos: verás el mensaje con el motivo de cierre. Después prueba cambiar la temporalidad — el reason será diferente.
void OnDeinit(const int reason) { // 1. Liberar TODOS los handles creados en OnInit if(g_maHandle != INVALID_HANDLE) { IndicatorRelease(g_maHandle); Print("Handle MA liberado correctamente"); } // 2. Identificar y loggear el motivo de cierre string reasonText = ""; switch(reason) { case REASON_REMOVE: reasonText = "EA eliminado del gráfico"; break; case REASON_CHARTCLOSE: reasonText = "Gráfico cerrado"; break; case REASON_RECOMPILE: reasonText = "EA recompilado"; break; case REASON_CHARTCHANGE: reasonText = "Cambio de símbolo/temporalidad"; break; case REASON_PARAMETERS: reasonText = "Parámetros modificados"; break; case REASON_ACCOUNT: reasonText = "Cambio de cuenta"; break; case REASON_CLOSE: reasonText = "Terminal cerrado"; break; default: reasonText = "Otro motivo: " + (string)reason; } Print("EA detenido. Motivo: ", reasonText); // 3. Limpiar objetos gráficos si los hubiera ObjectsDeleteAll(0, "EA_"); // Borra todos los objetos con prefijo "EA_" }
05 OnTester() — El criterio personalizado de optimización
OnTester() es la función más ignorada por principiantes y la más poderosa para traders avanzados. Solo se ejecuta al final de cada run de backtest en el Strategy Tester. Retorna un double — ese número se convierte en el criterio de optimización cuando seleccionas "Custom criterion". Puedes usarlo para crear métricas propias: profit factor ajustado, Sharpe con penalización por drawdown, o cualquier fórmula que represente mejor tu definición de "buena estrategia".
Este criterio premia estrategias con buen profit factor pero penaliza el drawdown. En el Strategy Tester, selecciona "Custom criterion" en el campo de optimización. El valor más alto ganará.
double OnTester() { // Obtener métricas del backtest recién completado double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR); double maxDrawdown = TesterStatistics(STAT_EQUITYDD_PERCENT) / 100.0; double totalTrades = TesterStatistics(STAT_TRADES); double winRate = TesterStatistics(STAT_PROFIT_TRADES) / MathMax(totalTrades, 1); // Filtros mínimos — descartar configuraciones inviables if(totalTrades < 30) return(0); // Muy pocos trades, no significativo if(profitFactor < 1.1) return(0); // Profit factor mínimo aceptable if(maxDrawdown > 0.20) return(0); // Drawdown máximo del 20% // Criterio compuesto: PF × Win Rate × (1 - Drawdown) // Maximiza rentabilidad y precisión, penaliza el riesgo double score = profitFactor * winRate * (1.0 - maxDrawdown); return(score); // Valor más alto = mejor configuración }
06 Las 4 funciones juntas — EA completo de referencia
Aquí está el esqueleto que puedes usar como punto de partida para cualquier EA. Incluye las cuatro funciones con el patrón correcto, manejo de errores, y comentarios que explican cada decisión. En el siguiente episodio reemplazaremos los comentarios de "aquí va la lógica" con el modelo de órdenes de MT5.
#property copyright "TradingNote" #property version "1.00" #property strict // ════════════════════════════════════════════════════════════════ // PARÁMETROS DE ENTRADA // ════════════════════════════════════════════════════════════════ input int InpMAPeriod = 20; // Período Media Móvil input int InpRSIPeriod= 14; // Período RSI input double InpLots = 0.10; // Tamaño del lote input int InpSLPips = 30; // Stop Loss en pips input int InpTPPips = 60; // Take Profit en pips // ════════════════════════════════════════════════════════════════ // VARIABLES GLOBALES // ════════════════════════════════════════════════════════════════ int g_maHandle; int g_rsiHandle; datetime g_lastBarTime = 0; // ════════════════════════════════════════════════════════════════ // OnInit — UNA VEZ al cargar // ════════════════════════════════════════════════════════════════ int OnInit() { if(InpMAPeriod < 2 || InpRSIPeriod < 2) return(INIT_PARAMETERS_INCORRECT); g_maHandle = iMA(_Symbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); g_rsiHandle = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE); if(g_maHandle==INVALID_HANDLE || g_rsiHandle==INVALID_HANDLE) { Print("ERROR creando handles: ", GetLastError()); return(INIT_FAILED); } Print("EA iniciado | ", _Symbol, " | MA:", InpMAPeriod, " RSI:", InpRSIPeriod); return(INIT_SUCCEEDED); } // ════════════════════════════════════════════════════════════════ // OnTick — CADA TICK del mercado // ════════════════════════════════════════════════════════════════ void OnTick() { // Filtro de nueva vela datetime barTime = (datetime)SeriesInfoInteger(_Symbol,_Period,SERIES_LASTBAR_DATE); if(barTime == g_lastBarTime) return; g_lastBarTime = barTime; // Leer indicadores double ma[2], rsi[2]; ArraySetAsSeries(ma,true); ArraySetAsSeries(rsi,true); if(CopyBuffer(g_maHandle, 0,0,2,ma) < 2) return; if(CopyBuffer(g_rsiHandle,0,0,2,rsi) < 2) return; double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // ── Aquí va tu lógica de señales ── // Episodio 3: aprenderás a enviar órdenes con CTrade } // ════════════════════════════════════════════════════════════════ // OnDeinit — UNA VEZ al remover // ════════════════════════════════════════════════════════════════ void OnDeinit(const int reason) { IndicatorRelease(g_maHandle); IndicatorRelease(g_rsiHandle); Print("EA detenido. Reason: ", EnumToString((ENUM_DEINIT_REASON)reason)); } // ════════════════════════════════════════════════════════════════ // OnTester — AL FINALIZAR cada backtest (optimización) // ════════════════════════════════════════════════════════════════ double OnTester() { double pf = TesterStatistics(STAT_PROFIT_FACTOR); double dd = TesterStatistics(STAT_EQUITYDD_PERCENT) / 100.0; double tr = TesterStatistics(STAT_TRADES); if(tr < 30 || pf < 1.1) return(0); return(pf * (1.0 - dd)); // Score compuesto }
CTrade hace que enviar y gestionar órdenes sea limpio y seguro.
◆ Conclusión
¿Listo para mejorar tu trading con datos?
Registra tus operaciones, analiza tu rendimiento y toma decisiones basadas en métricas reales con TradingNote.
Comentarios