//+------------------------------------------------------------------+ //| PainGain999_Hybrid_EA.mq5 | //| Estrategia híbrida 80% Trend / 20% Spike | //| Para Weltrade GainX 999 / PainX 999 | //| | //| LÓGICA DEL INSTRUMENTO: | //| - GainX 999: precio sube continuo, DROP aprox cada 999 ticks | //| - PainX 999: precio baja continuo, JUMP aprox cada 999 ticks | //| | //| ADVERTENCIA: Probar EN DEMO mínimo 2 semanas antes de real. | //| Apalancamiento 999% amplifica pérdidas tanto como ganancias. | //+------------------------------------------------------------------+ #property copyright "Andres - Bot híbrido PainX/GainX 999" #property version "1.00" #property strict #include #include #include //================ PARÁMETROS DE USUARIO ================// input group "=== Gestión de Riesgo ===" input double RiskPercentPerTrade = 1.0; // % de balance a arriesgar por trade input double MaxDailyDrawdownPct = 5.0; // Drawdown máximo diario (apaga el bot) input int MaxConsecutiveLosses = 3; // Pausa tras N pérdidas seguidas input int PauseMinutesAfterLosses = 60; // Minutos de pausa input group "=== Estrategia Trend (80% del capital) ===" input bool EnableTrendModule = true; input double TrendCapitalPct = 80.0; // % del riesgo asignado a trend input int MinTicksAfterSpike = 50; // Esperar N ticks tras spike antes de entrar input int MaxTicksForEntry = 600; // No entrar si ya pasaron N ticks (spike inminente) input int KillSwitchTicks = 800; // Cerrar TODO si se superan estos ticks input int EMA_Fast = 9; input int EMA_Slow = 21; input int EMA_Trail = 20; // EMA para trailing stop input int ATR_Period = 14; input double ATR_Multiplier_SL = 1.5; // SL = ATR x este valor input double TP1_RR_Ratio = 1.5; // TP1 en múltiplo de R input double TP1_ClosePercent = 50.0; // % a cerrar en TP1 input group "=== Estrategia Spike-Hunter (20% del capital) ===" input bool EnableSpikeModule = true; input double SpikeCapitalPct = 20.0; input int SpikeEntryTicks = 850; // Entrar pendiente tras N ticks input double SpikeTargetPoints = 0.0; // Puntos objetivo (0 = auto-calcular) input double SpikeTP1_ClosePercent = 70.0; // % a cerrar en TP1 del spike input int SpikeSL_Candles = 2; // SL = últimas N velas M1 input group "=== Instrumentos y Timeframe ===" input string Symbol_GainX999 = "GainX 999"; input string Symbol_PainX999 = "PainX 999"; input ENUM_TIMEFRAMES EntryTimeframe = PERIOD_M1; input int MagicNumber_Trend = 99901; input int MagicNumber_Spike = 99902; input group "=== Logging ===" input bool VerboseLogging = true; //================ VARIABLES GLOBALES ================// CTrade trade; CPositionInfo posInfo; CSymbolInfo symbolInfo; // Contadores de ticks por símbolo (clave: detectar proximidad al spike) struct SymbolState { string name; int ticksSinceLastSpike; double lastPrice; double lastSpikeSize; datetime lastSpikeTime; bool isGainX; // true = sube y cae (long-bias), false = baja y sube (short-bias) }; SymbolState gainXState; SymbolState painXState; // Handles de indicadores int hEMA_Fast_GainX, hEMA_Slow_GainX, hEMA_Trail_GainX, hATR_GainX; int hEMA_Fast_PainX, hEMA_Slow_PainX, hEMA_Trail_PainX, hATR_PainX; // Tracking de pérdidas int consecutiveLosses = 0; datetime pauseUntil = 0; double dailyStartBalance = 0; datetime currentDay = 0; //+------------------------------------------------------------------+ //| OnInit | //+------------------------------------------------------------------+ int OnInit() { // Inicializar estados gainXState.name = Symbol_GainX999; gainXState.isGainX = true; gainXState.ticksSinceLastSpike = 0; gainXState.lastPrice = 0; painXState.name = Symbol_PainX999; painXState.isGainX = false; painXState.ticksSinceLastSpike = 0; painXState.lastPrice = 0; // Validar que los símbolos existen if(!SymbolSelect(Symbol_GainX999, true)) { Print("ERROR: No se pudo seleccionar ", Symbol_GainX999, ". Verifica el nombre exacto en Weltrade."); return INIT_FAILED; } if(!SymbolSelect(Symbol_PainX999, true)) { Print("ERROR: No se pudo seleccionar ", Symbol_PainX999); return INIT_FAILED; } // Crear handles de indicadores hEMA_Fast_GainX = iMA(Symbol_GainX999, EntryTimeframe, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE); hEMA_Slow_GainX = iMA(Symbol_GainX999, EntryTimeframe, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE); hEMA_Trail_GainX = iMA(Symbol_GainX999, EntryTimeframe, EMA_Trail, 0, MODE_EMA, PRICE_CLOSE); hATR_GainX = iATR(Symbol_GainX999, EntryTimeframe, ATR_Period); hEMA_Fast_PainX = iMA(Symbol_PainX999, EntryTimeframe, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE); hEMA_Slow_PainX = iMA(Symbol_PainX999, EntryTimeframe, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE); hEMA_Trail_PainX = iMA(Symbol_PainX999, EntryTimeframe, EMA_Trail, 0, MODE_EMA, PRICE_CLOSE); hATR_PainX = iATR(Symbol_PainX999, EntryTimeframe, ATR_Period); if(hEMA_Fast_GainX == INVALID_HANDLE || hATR_GainX == INVALID_HANDLE || hEMA_Fast_PainX == INVALID_HANDLE || hATR_PainX == INVALID_HANDLE) { Print("ERROR: No se pudieron crear los handles de indicadores."); return INIT_FAILED; } dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE); currentDay = TimeCurrent() / 86400; trade.SetExpertMagicNumber(MagicNumber_Trend); trade.SetDeviationInPoints(10); trade.SetTypeFillingBySymbol(Symbol_GainX999); Print("=== PainGain999 Hybrid EA inicializado ==="); Print("Balance inicial del día: ", dailyStartBalance); Print("Riesgo por trade: ", RiskPercentPerTrade, "%"); Print("Max drawdown diario: ", MaxDailyDrawdownPct, "%"); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| OnDeinit | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { IndicatorRelease(hEMA_Fast_GainX); IndicatorRelease(hEMA_Slow_GainX); IndicatorRelease(hEMA_Trail_GainX); IndicatorRelease(hATR_GainX); IndicatorRelease(hEMA_Fast_PainX); IndicatorRelease(hEMA_Slow_PainX); IndicatorRelease(hEMA_Trail_PainX); IndicatorRelease(hATR_PainX); } //+------------------------------------------------------------------+ //| OnTick — cerebro principal | //+------------------------------------------------------------------+ void OnTick() { // 1. Actualizar contadores de ticks (detección de spike) UpdateSymbolState(gainXState); UpdateSymbolState(painXState); // 2. Kill switches globales if(!CheckGlobalSafety()) return; // 3. Trailing y gestión de posiciones abiertas ManageOpenPositions(gainXState); ManageOpenPositions(painXState); // 4. Buscar nuevas entradas if(EnableTrendModule) { CheckTrendEntry(gainXState); CheckTrendEntry(painXState); } if(EnableSpikeModule) { CheckSpikeEntry(gainXState); CheckSpikeEntry(painXState); } } //+------------------------------------------------------------------+ //| Detectar spike y actualizar contador de ticks | //+------------------------------------------------------------------+ void UpdateSymbolState(SymbolState &state) { MqlTick tick; if(!SymbolInfoTick(state.name, tick)) return; double currentPrice = tick.bid; if(state.lastPrice == 0) { state.lastPrice = currentPrice; return; } double priceChange = currentPrice - state.lastPrice; double point = SymbolInfoDouble(state.name, SYMBOL_POINT); double changeInPoints = MathAbs(priceChange) / point; // Detección de spike: un movimiento anormalmente grande en 1 tick // En GainX, el spike es a la BAJA (drop). En PainX, es al ALZA (jump). // Umbral: un spike normalmente equivale a 50x+ el tick normal double spikeThreshold = 200; // puntos — ajustar tras observar data real bool spikeDetected = false; if(state.isGainX && priceChange < 0 && changeInPoints > spikeThreshold) { spikeDetected = true; } if(!state.isGainX && priceChange > 0 && changeInPoints > spikeThreshold) { spikeDetected = true; } if(spikeDetected) { state.lastSpikeSize = changeInPoints; state.lastSpikeTime = TimeCurrent(); state.ticksSinceLastSpike = 0; if(VerboseLogging) { Print("SPIKE detectado en ", state.name, " | tamaño: ", changeInPoints, " pts"); } } else { state.ticksSinceLastSpike++; } state.lastPrice = currentPrice; } //+------------------------------------------------------------------+ //| Safety global: drawdown diario, pérdidas seguidas, kill switch | //+------------------------------------------------------------------+ bool CheckGlobalSafety() { // Reset diario datetime dayNow = TimeCurrent() / 86400; if(dayNow != currentDay) { currentDay = dayNow; dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE); consecutiveLosses = 0; Print("Nuevo día. Balance inicial: ", dailyStartBalance); } // Drawdown diario double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); double dd = (dailyStartBalance - currentEquity) / dailyStartBalance * 100.0; if(dd >= MaxDailyDrawdownPct) { CloseAllPositions("Daily drawdown limit hit"); Print("!!! KILL SWITCH: Drawdown diario ", dd, "% superó el límite. Bot apagado."); ExpertRemove(); return false; } // Pausa por pérdidas seguidas if(TimeCurrent() < pauseUntil) return false; // Kill switch por ticks (cerrar todo si estamos en zona de spike inminente) if(gainXState.ticksSinceLastSpike >= KillSwitchTicks) { ClosePositionsBySymbolAndMagic(Symbol_GainX999, MagicNumber_Trend, "Kill switch ticks"); } if(painXState.ticksSinceLastSpike >= KillSwitchTicks) { ClosePositionsBySymbolAndMagic(Symbol_PainX999, MagicNumber_Trend, "Kill switch ticks"); } return true; } //+------------------------------------------------------------------+ //| ENTRADA TREND — montar la tendencia natural | //+------------------------------------------------------------------+ void CheckTrendEntry(SymbolState &state) { // Ya hay posición trend abierta en este símbolo? if(HasOpenPosition(state.name, MagicNumber_Trend)) return; // Estamos en la "ventana segura" (lejos del último spike pero no demasiado)? if(state.ticksSinceLastSpike < MinTicksAfterSpike) return; if(state.ticksSinceLastSpike > MaxTicksForEntry) return; // Confluencia con EMAs double emaFast[], emaSlow[]; int hFast = state.isGainX ? hEMA_Fast_GainX : hEMA_Fast_PainX; int hSlow = state.isGainX ? hEMA_Slow_GainX : hEMA_Slow_PainX; if(CopyBuffer(hFast, 0, 0, 2, emaFast) < 2) return; if(CopyBuffer(hSlow, 0, 0, 2, emaSlow) < 2) return; bool emaAligned = false; if(state.isGainX) { // GainX → solo BUY. EMA rápida debe estar sobre la lenta emaAligned = (emaFast[0] > emaSlow[0]); } else { // PainX → solo SELL. EMA rápida debe estar bajo la lenta emaAligned = (emaFast[0] < emaSlow[0]); } if(!emaAligned) return; // Calcular SL con ATR double atr[]; int hATR = state.isGainX ? hATR_GainX : hATR_PainX; if(CopyBuffer(hATR, 0, 0, 1, atr) < 1) return; double slDistance = atr[0] * ATR_Multiplier_SL; if(slDistance <= 0) return; // Calcular tamaño de lote según riesgo double lotSize = CalculateLotSize(state.name, slDistance, TrendCapitalPct); if(lotSize <= 0) return; // Ejecutar orden trade.SetExpertMagicNumber(MagicNumber_Trend); MqlTick tick; SymbolInfoTick(state.name, tick); double entryPrice, sl, tp; if(state.isGainX) { entryPrice = tick.ask; sl = entryPrice - slDistance; tp = entryPrice + (slDistance * TP1_RR_Ratio); if(trade.Buy(lotSize, state.name, entryPrice, sl, tp, "Trend-Long " + state.name)) { Print("TREND BUY ", state.name, " @ ", entryPrice, " SL:", sl, " TP1:", tp); } } else { entryPrice = tick.bid; sl = entryPrice + slDistance; tp = entryPrice - (slDistance * TP1_RR_Ratio); if(trade.Sell(lotSize, state.name, entryPrice, sl, tp, "Trend-Short " + state.name)) { Print("TREND SELL ", state.name, " @ ", entryPrice, " SL:", sl, " TP1:", tp); } } } //+------------------------------------------------------------------+ //| ENTRADA SPIKE-HUNTER — cazar el jump/drop | //+------------------------------------------------------------------+ void CheckSpikeEntry(SymbolState &state) { if(HasOpenPosition(state.name, MagicNumber_Spike)) return; if(state.ticksSinceLastSpike < SpikeEntryTicks) return; if(state.ticksSinceLastSpike >= KillSwitchTicks) return; // ya es muy tarde // SL basado en últimas N velas M1 double high = iHigh(state.name, PERIOD_M1, 1); double low = iLow(state.name, PERIOD_M1, 1); for(int i = 2; i <= SpikeSL_Candles; i++) { high = MathMax(high, iHigh(state.name, PERIOD_M1, i)); low = MathMin(low, iLow(state.name, PERIOD_M1, i)); } double candleRange = high - low; if(candleRange <= 0) return; // Target del spike: tamaño promedio del último spike observado, o default double spikeTarget = SpikeTargetPoints; if(spikeTarget == 0 && state.lastSpikeSize > 0) { spikeTarget = state.lastSpikeSize * SymbolInfoDouble(state.name, SYMBOL_POINT) * 0.7; } if(spikeTarget <= 0) return; double lotSize = CalculateLotSize(state.name, candleRange, SpikeCapitalPct); if(lotSize <= 0) return; trade.SetExpertMagicNumber(MagicNumber_Spike); MqlTick tick; SymbolInfoTick(state.name, tick); double entryPrice, sl, tp; if(state.isGainX) { // GainX el spike es DROP → entramos SELL esperando la caída entryPrice = tick.bid; sl = high + (candleRange * 0.1); tp = entryPrice - spikeTarget; if(trade.Sell(lotSize, state.name, entryPrice, sl, tp, "Spike-Hunt " + state.name)) { Print("SPIKE SELL ", state.name, " @ ", entryPrice, " SL:", sl, " TP:", tp); } } else { // PainX el spike es JUMP → entramos BUY esperando la subida entryPrice = tick.ask; sl = low - (candleRange * 0.1); tp = entryPrice + spikeTarget; if(trade.Buy(lotSize, state.name, entryPrice, sl, tp, "Spike-Hunt " + state.name)) { Print("SPIKE BUY ", state.name, " @ ", entryPrice, " SL:", sl, " TP:", tp); } } } //+------------------------------------------------------------------+ //| Gestión de posiciones abiertas: parcial, BE, trailing | //+------------------------------------------------------------------+ void ManageOpenPositions(SymbolState &state) { for(int i = PositionsTotal() - 1; i >= 0; i--) { if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() != state.name) continue; long magic = posInfo.Magic(); if(magic != MagicNumber_Trend && magic != MagicNumber_Spike) continue; double openPrice = posInfo.PriceOpen(); double currentSL = posInfo.StopLoss(); double currentTP = posInfo.TakeProfit(); double currentPrice = posInfo.PriceCurrent(); ENUM_POSITION_TYPE posType = posInfo.PositionType(); // Trailing con EMA20 para la porción que queda tras TP1 double emaTrail[]; int hTrail = state.isGainX ? hEMA_Trail_GainX : hEMA_Trail_PainX; if(CopyBuffer(hTrail, 0, 0, 1, emaTrail) < 1) continue; double newSL = currentSL; bool shouldModify = false; if(posType == POSITION_TYPE_BUY) { // Si el precio ya superó TP1 y el SL sigue abajo del break-even, súbelo a BE // (el parcial se toma automáticamente con el TP original del broker) double beLevel = openPrice + (10 * SymbolInfoDouble(state.name, SYMBOL_POINT)); if(currentPrice >= currentTP && currentSL < beLevel && currentTP > 0) { newSL = openPrice; // breakeven shouldModify = true; } // Trailing con EMA20 una vez en profit if(currentPrice > openPrice && emaTrail[0] > currentSL && emaTrail[0] < currentPrice) { newSL = emaTrail[0]; shouldModify = true; } } else if(posType == POSITION_TYPE_SELL) { double beLevel = openPrice - (10 * SymbolInfoDouble(state.name, SYMBOL_POINT)); if(currentPrice <= currentTP && (currentSL > beLevel || currentSL == 0) && currentTP > 0) { newSL = openPrice; shouldModify = true; } if(currentPrice < openPrice && emaTrail[0] < currentSL && emaTrail[0] > currentPrice) { newSL = emaTrail[0]; shouldModify = true; } } // Cierre parcial al tocar TP1 (una sola vez) if((posType == POSITION_TYPE_BUY && currentPrice >= currentTP && currentTP > 0) || (posType == POSITION_TYPE_SELL && currentPrice <= currentTP && currentTP > 0)) { double closePct = (magic == MagicNumber_Spike) ? SpikeTP1_ClosePercent : TP1_ClosePercent; double volumeToClose = NormalizeLot(state.name, posInfo.Volume() * closePct / 100.0); if(volumeToClose > 0 && volumeToClose < posInfo.Volume()) { if(trade.PositionClosePartial(posInfo.Ticket(), volumeToClose)) { Print("Parcial ", closePct, "% cerrado en ", state.name, " | magic: ", magic); // Tras parcial: limpiar TP, mover SL a BE y dejar trailing trade.PositionModify(posInfo.Ticket(), openPrice, 0); } } } if(shouldModify && newSL != currentSL) { trade.PositionModify(posInfo.Ticket(), newSL, currentTP); } } } //+------------------------------------------------------------------+ //| UTILIDADES | //+------------------------------------------------------------------+ double CalculateLotSize(string symbol, double slDistance, double capitalAllocationPct) { double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); double allocatedCapital = accountBalance * (capitalAllocationPct / 100.0); double riskAmount = allocatedCapital * (RiskPercentPerTrade / 100.0); double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); if(tickSize == 0) return 0; double lossPerLot = (slDistance / tickSize) * tickValue; if(lossPerLot <= 0) return 0; double lot = riskAmount / lossPerLot; return NormalizeLot(symbol, lot); } double NormalizeLot(string symbol, double lot) { double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); lot = MathFloor(lot / lotStep) * lotStep; if(lot < minLot) lot = minLot; if(lot > maxLot) lot = maxLot; return NormalizeDouble(lot, 2); } bool HasOpenPosition(string symbol, int magic) { for(int i = 0; i < PositionsTotal(); i++) { if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() == symbol && posInfo.Magic() == magic) return true; } return false; } void CloseAllPositions(string reason) { for(int i = PositionsTotal() - 1; i >= 0; i--) { if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Magic() == MagicNumber_Trend || posInfo.Magic() == MagicNumber_Spike) { trade.PositionClose(posInfo.Ticket()); Print("Cierre forzado: ", reason, " ticket ", posInfo.Ticket()); } } } void ClosePositionsBySymbolAndMagic(string symbol, int magic, string reason) { for(int i = PositionsTotal() - 1; i >= 0; i--) { if(!posInfo.SelectByIndex(i)) continue; if(posInfo.Symbol() == symbol && posInfo.Magic() == magic) { trade.PositionClose(posInfo.Ticket()); Print("Cierre: ", reason, " ticket ", posInfo.Ticket()); } } } //+------------------------------------------------------------------+ //| OnTrade: tracking de pérdidas consecutivas | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return; HistorySelect(TimeCurrent() - 86400, TimeCurrent() + 60); ulong dealTicket = trans.deal; if(HistoryDealSelect(dealTicket)) { double profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); long entryType = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); if(entryType == DEAL_ENTRY_OUT) { // deal de cierre if(profit < 0) { consecutiveLosses++; if(consecutiveLosses >= MaxConsecutiveLosses) { pauseUntil = TimeCurrent() + (PauseMinutesAfterLosses * 60); Print("!!! Pausa activada por ", MaxConsecutiveLosses, " pérdidas seguidas. Hasta: ", TimeToString(pauseUntil)); consecutiveLosses = 0; } } else if(profit > 0) { consecutiveLosses = 0; } } } } //+------------------------------------------------------------------+