Construye tu Primer Asesor Experto — Cruce de Medias Móviles Paso a Paso
Ya tienes el entorno configurado y la referencia MQL5 a mano. Ahora es momento de construir un EA real: una estrategia de cruce de medias móviles. Al final de este artículo tendrás un EA completo de cruce MA que compila, se ejecuta en el Probador de Estrategias y coloca órdenes de compra/venta reales — con reglas claras de entrada/salida y lógica que puedes ampliar. Este tutorial te lleva de la idea al código en ocho pasos concretos.
Todo el código sigue la documentación oficial de manejadores de eventos MQL5 y funciones de trading.
La Estrategia — Cruce de Medias Móviles
Idea: Usar dos medias móviles — una rápida (ej. 20 períodos) y una lenta (ej. 50 períodos).
- Señal de compra: La MA rápida cruza por encima de la lenta
- Señal de venta: La MA rápida cruza por debajo de la lenta
Es un enfoque clásico de seguimiento de tendencia. Operamos en la dirección del cruce.
Paso 1: Definir la Lógica
Antes de escribir una sola línea de MQL5, debes concretar tu lógica de trading. Una especificación clara evita errores y facilita el depurado. Para nuestro EA de cruce de MA, definimos cada elección explícitamente.
| Componente | Elección | Por qué esta elección |
|---|---|---|
| MA rápida | EMA 20 períodos, Cierre | La EMA reacciona más rápido que la SMA a precios recientes — ideal para cruces. Cierre es estándar y muy usado en backtests. |
| MA lenta | EMA 50 períodos, Cierre | 50 es un período "medio plazo" habitual; suaviza el ruido y confirma la dirección de la tendencia. |
| Entrada | Una posición a la vez; señal nueva cierra la opuesta y abre nueva | Mantiene el riesgo simple: sin posiciones superpuestas. Al nuevo cruce, cerramos la posición opuesta y abrimos la nueva. |
| Salida | Cruce opuesto cierra la posición | No hay regla de salida aparte — el siguiente cruce opuesto cierra la operación actual y abre la contraria. |
Consejo: Documenta así tu lógica en cada EA. Tu yo futuro (y Google) te lo agradecerán cuando revises el código meses después.
Paso 2: Crear el Archivo del EA
Crea un nuevo Asesor Experto desde la plantilla de MetaEditor para obtener la estructura correcta (OnInit, OnTick, OnDeinit) desde el inicio.
- En MetaEditor, ve a Archivo — Nuevo y elige Asesor Experto (plantilla).
- Cuando lo pida, nómbralo
MACrossoverEAy deja los parámetros por defecto — el asistente crea el esqueleto. - Guarda el archivo en MQL5\Experts. MetaTrader 5 busca los EAs en esta carpeta. Si guardas en otro sitio, el EA no aparecerá en el Navegador.
Estructura de carpetas: MetaTrader 5 se instala en una ruta como esta:
C:\Users\TuNombre\AppData\Roaming\MetaQuotes\Terminal\...\MQL5\Experts
Tu archivo MACrossoverEA.mq5 debe estar ahí (o en una subcarpeta) para compilarse y ejecutarse.
Paso 3: Entradas y Handles
Declaramos dos tipos de variables: inputs (configurables por el usuario) y handles (referencias a instancias del indicador).
Los inputs aparecen en el diálogo de propiedades del EA y en el Probador de Estrategias. El usuario puede cambiar períodos y lote sin tocar el código. La palabra clave input los hace persistentes.
Los handles son IDs enteros devueltos por iMA(). Apuntan a los buffers internos del indicador. Los creamos una vez en OnInit() y los reutilizamos en OnTick() con CopyBuffer(). Nunca crees handles dentro de OnTick() — eso asignaría nuevos indicadores en cada tick y dañaría el rendimiento.
Las directivas #property definen metadatos mostrados en MetaTrader (copyright, versión). Son opcionales pero buena práctica en EAs profesionales.
#property copyright "AlfaTactix Academy"
#property version "1.00"
input int InpFastPeriod = 20; // Período MA rápida
input int InpSlowPeriod = 50; // Período MA lenta
input double InpLotSize = 0.1; // Tamaño del lote
input int InpMagic = 12345; // Número mágico
int g_fastHandle = INVALID_HANDLE;
int g_slowHandle = INVALID_HANDLE;
Paso 4: OnInit — Crear Handles de Indicadores
OnInit() se ejecuta una vez cuando el EA se adjunta al gráfico (o cuando arranca el Probador de Estrategias). Según la documentación OnInit, aquí se crean los handles de indicadores, se validan los inputs y se preparan los recursos.
Validación de inputs: Rechazamos períodos inválidos (p.ej. rápida ≥ lenta, o cero/negativos) y devolvemos INIT_PARAMETERS_INCORRECT. Así el EA se detiene limpio y muestra un error en la pestaña Expertos en vez de fallar más tarde.
Parámetros de iMA: iMA(symbol, timeframe, period, shift, method, applied_price). Usamos PERIOD_CURRENT para que el EA siga el timeframe del gráfico, MODE_EMA para la media exponencial y PRICE_CLOSE para precios de cierre. Si iMA() devuelve INVALID_HANDLE, el terminal registra el error y devolvemos INIT_FAILED.
OnDeinit: Al quitar el EA (gráfico cerrado, probador terminado), OnDeinit() libera los handles con IndicatorRelease(). Así se libera memoria y se evitan fugas — imprescindible al ejecutar muchos backtests.
int OnInit()
{
if(InpFastPeriod <= 0 || InpSlowPeriod <= 0 || InpFastPeriod >= InpSlowPeriod)
{
Print("Períodos de MA inválidos. Rápida debe ser < Lenta.");
return(INIT_PARAMETERS_INCORRECT);
}
g_fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
g_slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(g_fastHandle == INVALID_HANDLE || g_slowHandle == INVALID_HANDLE)
{
Print("Error al crear handles de MA. Error: ", GetLastError());
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(g_fastHandle != INVALID_HANDLE) IndicatorRelease(g_fastHandle);
if(g_slowHandle != INVALID_HANDLE) IndicatorRelease(g_slowHandle);
}
Paso 5: OnTick — Detectar Cruce y Operar
OnTick() se llama en cada tick de precio. Eso puede ser cientos de veces por vela. Si evaluamos señales en cada tick, corremos el riesgo de órdenes duplicadas y múltiples entradas en la misma barra. La solución: procesar señales solo cuando se forma una nueva barra.
Comprobación de nueva barra: Comparamos iTime(_Symbol, PERIOD_CURRENT, 0) (tiempo de apertura de la barra actual) con una variable static lastBar. Cuando difieren, se ha formado una nueva barra. Actualizamos lastBar y seguimos; si no, salimos de inmediato. Es un patrón ligero y fiable usado en EAs profesionales.
¿Por qué tres barras? Para detectar un cruce necesitamos el estado antes y después del cruce. Con ArraySetAsSeries(buffer, true), índice 0 = barra actual, 1 = barra anterior (cerrada), 2 = barra anterior a esa. El cruce ocurre entre las barras 2 y 1:
- Compra: En barra 1 la MA rápida está por encima de la lenta (
fast[1] > slow[1]), pero en barra 2 estaba por debajo o igual (fast[2] <= slow[2]). Eso significa que la rápida cruzó por encima de la lenta. - Venta: Simétricamente,
fast[1] < slow[1]yfast[2] >= slow[2]indica que la rápida cruzó por debajo.
CopyBuffer: Pedimos 3 elementos (índices 0, 1, 2). Si CopyBuffer devuelve menos de 3, aún no hay suficiente historial — salimos y esperamos al siguiente tick. En un gráfico reciente, las primeras barras pueden no tener datos suficientes; por eso este guarda es esencial.
Flujo: En señal de compra cerramos primero las ventas, luego abrimos compra si no tenemos ya una. Igual para venta. Así mantenemos la regla de "una posición por dirección".
void OnTick()
{
static datetime lastBar = 0;
datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBar == lastBar) return; // Esperar nueva barra
lastBar = currentBar;
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(g_fastHandle, 0, 0, 3, fast) < 3) return;
if(CopyBuffer(g_slowHandle, 0, 0, 3, slow) < 3) return;
bool buySignal = (fast[1] > slow[1]) && (fast[2] <= slow[2]);
bool sellSignal = (fast[1] < slow[1]) && (fast[2] >= slow[2]);
if(buySignal)
{
ClosePositions(POSITION_TYPE_SELL);
if(CountPositions(POSITION_TYPE_BUY) == 0)
OpenBuy();
}
else if(sellSignal)
{
ClosePositions(POSITION_TYPE_BUY);
if(CountPositions(POSITION_TYPE_SELL) == 0)
OpenSell();
}
}
Paso 6: OpenBuy y OpenSell — OrderSend
OrderSend() envía una solicitud de trading al bróker. Rellenas la estructura MqlTradeRequest y opcionalmente recibes el resultado en MqlTradeResult.
Campos clave explicados:
- action = TRADE_ACTION_DEAL — Ejecución inmediata a mercado (no orden pendiente).
- symbol — El par de trading;
_Symboles el símbolo del gráfico. - volume — Tamaño del lote; debe cumplir
SYMBOL_VOLUME_MINySYMBOL_VOLUME_STEP. - type —
ORDER_TYPE_BUYoORDER_TYPE_SELL. - price — Para órdenes a mercado: usa Ask para compras (pagas el ask) y Bid para ventas (recibes el bid).
SymbolInfoDouble(_Symbol, SYMBOL_ASK/BID)devuelve el precio actual. - deviation — Deslizamiento máximo permitido en puntos. 10 puntos es un valor por defecto habitual; aumenta en símbolos volátiles si ves requotes.
- magic — Identificador de tu EA. Úsalo para filtrar posiciones y órdenes en
PositionSelect,OrderSelect, etc.
Manejo de errores: Si OrderSend() devuelve false, GetLastError() da la razón (p.ej. margen insuficiente, mercado cerrado, requote). Siempre regístralo para depurar.
void OpenBuy()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_BUY;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res))
Print("OpenBuy falló: ", GetLastError());
}
void OpenSell()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_SELL;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res))
Print("OpenSell falló: ", GetLastError());
}
Paso 7: Cerrar Posiciones y Contar por Magic
En MetaTrader 5, una cuenta puede tener posiciones de varios EAs y operaciones manuales. Debemos filtrar por símbolo y número mágico para que nuestro EA gestione solo sus propias posiciones.
CountPositions: Recorre PositionsTotal() — el número de posiciones abiertas. PositionGetTicket(i) devuelve el ticket; PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC) y PositionGetInteger(POSITION_TYPE) identifican cada posición. Contamos solo las que coinciden con nuestro símbolo, magic y tipo (BUY o SELL). Así evitamos abrir una segunda compra cuando ya tenemos una.
ClosePositions: Para cerrar una posición enviamos una contra-orden — venta para cerrar compra, o compra para cerrar venta. Configuramos req.action = TRADE_ACTION_DEAL, req.position = ticket (la posición a cerrar) y req.type / req.price al lado opuesto. req.volume debe igualar el volumen de la posición (cerramos toda). El magic asegura que la orden de cierre se atribuya a nuestro EA.
¿Por qué iterar hacia atrás? Al modificar la lista de posiciones (p.ej. cerrar una), los índices pueden desplazarse. Iterar de PositionsTotal()-1 a 0 evita saltar o procesar dos veces.
int CountPositions(ENUM_POSITION_TYPE type)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
count++;
}
return count;
}
void ClosePositions(ENUM_POSITION_TYPE type)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.position = ticket;
req.symbol = _Symbol;
req.volume = PositionGetDouble(POSITION_VOLUME);
req.deviation = 10;
req.magic = InpMagic;
req.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
req.price = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
OrderSend(req, res);
}
}
Paso 8: Compilar y Probar
1. Compilar. En MetaEditor, pulsa F7 (o Compilar en el menú Compilar). El panel de salida muestra errores y advertencias. Corrige los errores antes de seguir. Las advertencias (p.ej. variables no usadas) son opcionales pero conviene limpiarlas para código de producción.
2. Abrir el Probador de Estrategias. En MetaTrader 5, ve a Ver — Probador de Estrategias (o pulsa Ctrl+R). El panel aparece en la parte inferior.
3. Configurar la prueba. Selecciona tu EA (MACrossoverEA) en el desplegable. Elige símbolo (p.ej. EURUSD), timeframe (H1 recomendado para cruces) y rango de fechas (p.ej. últimos 12 meses). Ajusta depósito y apalancamiento si hace falta. Activa Modo visual si quieres ver el EA operar barra a barra.
4. Elegir calidad de modelado. "Cada tick" es lo más preciso pero más lento. "1 minuto OHLC" usa un tick por minuto y ofrece buen equilibrio velocidad/realismo para estrategias diarias/horarias. "Solo precios de apertura" es lo más rápido pero menos preciso — úsalo solo para comprobaciones rápidas.
5. Ejecutar la prueba. Haz clic en Iniciar. Al terminar, revisa la pestaña Resultados (beneficio, drawdown, operaciones) y la Diario (salida de Print() y errores). La pestaña Gráfico muestra la curva de equity. Usa esto para validar que tu EA se comporta como esperas antes de plantear Demo o Live.
Resumen — Lo Que Construiste
| Parte | Función |
|---|---|
| OnInit | Crear handles iMA, validar inputs |
| OnDeinit | Liberar handles |
| OnTick | Comprobar nueva barra, lógica de cruce, abrir/cerrar |
| OpenBuy/OpenSell | OrderSend con MqlTradeRequest |
| ClosePositions | Cerrar por símbolo + magic + tipo |
| CountPositions | Evitar posiciones duplicadas |
Código Completo
Aquí tienes el EA completo en un solo bloque para copiar y verificar. Ensámblalo en MetaEditor y guárdalo como MACrossoverEA.mq5 en MQL5\Experts.
//+------------------------------------------------------------------+
//| MACrossoverEA.mq5 |
//| EA de Cruce de Medias Móviles |
//+------------------------------------------------------------------+
#property copyright "AlfaTactix Academy"
#property version "1.00"
input int InpFastPeriod = 20; // Período MA rápida
input int InpSlowPeriod = 50; // Período MA lenta
input double InpLotSize = 0.1; // Tamaño del lote
input int InpMagic = 12345; // Número mágico
int g_fastHandle = INVALID_HANDLE;
int g_slowHandle = INVALID_HANDLE;
int OnInit()
{
if(InpFastPeriod <= 0 || InpSlowPeriod <= 0 || InpFastPeriod >= InpSlowPeriod)
{
Print("Períodos de MA inválidos. Rápida debe ser < Lenta.");
return(INIT_PARAMETERS_INCORRECT);
}
g_fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
g_slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(g_fastHandle == INVALID_HANDLE || g_slowHandle == INVALID_HANDLE)
{
Print("Error al crear handles de MA. Error: ", GetLastError());
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(g_fastHandle != INVALID_HANDLE) IndicatorRelease(g_fastHandle);
if(g_slowHandle != INVALID_HANDLE) IndicatorRelease(g_slowHandle);
}
void OnTick()
{
static datetime lastBar = 0;
datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBar == lastBar) return;
lastBar = currentBar;
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(g_fastHandle, 0, 0, 3, fast) < 3) return;
if(CopyBuffer(g_slowHandle, 0, 0, 3, slow) < 3) return;
bool buySignal = (fast[1] > slow[1]) && (fast[2] <= slow[2]);
bool sellSignal = (fast[1] < slow[1]) && (fast[2] >= slow[2]);
if(buySignal)
{
ClosePositions(POSITION_TYPE_SELL);
if(CountPositions(POSITION_TYPE_BUY) == 0) OpenBuy();
}
else if(sellSignal)
{
ClosePositions(POSITION_TYPE_BUY);
if(CountPositions(POSITION_TYPE_SELL) == 0) OpenSell();
}
}
void OpenBuy()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_BUY;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res)) Print("OpenBuy falló: ", GetLastError());
}
void OpenSell()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_SELL;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res)) Print("OpenSell falló: ", GetLastError());
}
int CountPositions(ENUM_POSITION_TYPE type)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
count++;
}
return count;
}
void ClosePositions(ENUM_POSITION_TYPE type)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.position = ticket;
req.symbol = _Symbol;
req.volume = PositionGetDouble(POSITION_VOLUME);
req.deviation = 10;
req.magic = InpMagic;
req.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
req.price = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
OrderSend(req, res);
}
}
Resolución de Problemas
| Síntoma | Causa | Solución |
|---|---|---|
INVALID_HANDLE de iMA | Símbolo o timeframe no disponible, o parámetros incorrectos | Asegúrate de que el símbolo esté en Market Watch, el gráfico abierto y los períodos válidos (rápida < lenta, ambos > 0). |
Not enough money (134) | Margen insuficiente para el lote | Reduce InpLotSize, aumenta el depósito en el Probador o revisa el apalancamiento. |
Requote (10004) | El precio cambió antes de ejecutar | Aumenta req.deviation (p.ej. 20–50 puntos) o acepta requotes y reintenta. |
| El EA no abre operaciones | AutoTrading desactivado o "Permitir live trading" sin marcar | Haz clic en AutoTrading en la barra de MT5 (Ctrl+E) y en Propiedades del EA activa "Permitir Algo Trading". |
Trade not allowed (64) | Restricciones del bróker o cuenta | Verifica que el símbolo permita trading, el mercado esté abierto y la cuenta permita trading automatizado. |
| Órdenes duplicadas en la misma barra | Sin comprobación de nueva barra en OnTick | Añade la comprobación lastBar / iTime del Paso 5 — evalúa señales solo una vez por vela. |
Revisa la pestaña Expertos en MetaTrader 5 para códigos de error y mensajes Print(). GetLastError() devuelve el código; consulta las constantes de error MQL5.
Próximos Pasos — Añadir Gestión de Riesgo
Este EA no tiene Stop Loss ni Take Profit. En Gestión de riesgo en EA añadirás dimensionamiento, SL/TP, trailing stops y buenas prácticas para proteger tu cuenta.
Consejo extra: ¿Quieres crear el mismo EA de cruce MA sin escribir código? Prueba AlfaTactix Strategy Builder gratis — diseña cruces de medias móviles, filtros RSI y reglas de riesgo de forma visual, y exporta MQL5 listo para producción en minutos. Compara variantes antes de refinar en MetaEditor. La misma lógica, cero tecleo.