//+------------------------------------------------------------------+ //| Expert Advisor: Pairs Trading EURCHF / AUDUSD | //+------------------------------------------------------------------+ #property strict #include CTrade trade; input string symbolA = "EURCHF"; input string symbolB = "AUDUSD"; input int atrPeriod = 14; input double entryZ = 2.0; input double exitZ = 1.0; input double stopZ = 3.0; input int maxBars = 24; input double K_riskPips = 70.0; input bool debugLogs = true; input double maxDailyDrawdownPercent = 2.5; input double maxEquityDrawdownPercent = 6.0; input double riskPerTradePercent = 1.0; input double profitExitPercent = 10.0; double hedgeRatio = 1.0; datetime entryTime; bool inPosition = false; double lastZ = 0.0; double peakEquity = 0.0; int tradeDay = -1; double dailyStartEquity = 0.0; double initialEntryEquity = 0.0; int OnInit() { peakEquity = AccountInfoDouble(ACCOUNT_EQUITY); return INIT_SUCCEEDED; } void OnTick() { // ✅ Ejecutar solo al cierre de una nueva vela H1 static datetime lastCheckedTime = 0; datetime closedBarTime = iTime(symbolA, PERIOD_H1, 1); if (closedBarTime == 0 || closedBarTime == lastCheckedTime) return; lastCheckedTime = closedBarTime; double equity = AccountInfoDouble(ACCOUNT_EQUITY); if (equity > peakEquity) peakEquity = equity; double equityDrawdown = 100.0 * (peakEquity - equity) / peakEquity; if (equityDrawdown > maxEquityDrawdownPercent) return; MqlDateTime now; TimeToStruct(TimeLocal(), now); int today = now.day; if (today != tradeDay) { tradeDay = today; dailyStartEquity = equity; } double dailyDrawdown = 100.0 * (dailyStartEquity - equity) / dailyStartEquity; if (dailyDrawdown > maxDailyDrawdownPercent) return; bool posA = PositionSelect(symbolA); bool posB = PositionSelect(symbolB); if (inPosition || posA || posB) { double floatingProfit = GetFloatingProfit(symbolA) + GetFloatingProfit(symbolB); double timeElapsed = TimeCurrent() - entryTime; bool maxBarsPassed = timeElapsed >= maxBars * 3600; double equityGainPercent = 0.0; if (initialEntryEquity > 0.0) equityGainPercent = 100.0 * (equity - initialEntryEquity) / initialEntryEquity; if ( MathAbs(lastZ) <= exitZ || MathAbs(lastZ) >= stopZ || (maxBarsPassed && floatingProfit > 0) || (maxBarsPassed && floatingProfit < 0) || (initialEntryEquity > 0.0 && equityGainPercent >= profitExitPercent) ) { if (debugLogs) Print("Cerrando posición. Z=", lastZ, " | Profit=", floatingProfit, " | Gain%=", equityGainPercent); CloseTrades(); } return; } if (iBars(symbolA, PERIOD_H1) < 100 || iBars(symbolB, PERIOD_H1) < 100) return; double pricesA[], pricesB[]; if (CopyClose(symbolA, PERIOD_H1, 0, 1000, pricesA) < 1000 || CopyClose(symbolB, PERIOD_H1, 0, 1000, pricesB) < 1000) return; ArraySetAsSeries(pricesA, true); ArraySetAsSeries(pricesB, true); hedgeRatio = CalculateHedgeRatio(pricesA, pricesB); if (!MathIsValidNumber(hedgeRatio) || hedgeRatio == 0.0) return; double spread[1000]; for (int i = 0; i < 1000; i++) spread[i] = pricesA[i] - hedgeRatio * pricesB[i]; double mean = MathMean(spread); double stdDev = MathStandardDeviation(spread, mean); if (!MathIsValidNumber(stdDev) || stdDev == 0.0) return; double currentSpread = pricesA[1] - hedgeRatio * pricesB[1]; // vela cerrada double z = (currentSpread - mean) / stdDev; if (debugLogs) Print("Z-score: ", z); lastZ = z; if (!inPosition && !posA && !posB) { if (z > entryZ) EnterTrade(true, z); else if (z < -entryZ) EnterTrade(false, z); } } void EnterTrade(bool longA, double z) { double atrA = GetATR(symbolA, atrPeriod); double atrB = GetATR(symbolB, atrPeriod); if (atrA <= 0 || atrB <= 0) return; double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPerTradePercent / 100.0; double stopLossPips = atrA * 2.0; double lotSize = NormalizeDouble(riskMoney / (stopLossPips * 10.0), 2); lotSize = MathMax(0.01, MathMin(0.2, lotSize)); double priceA = SymbolInfoDouble(symbolA, longA ? SYMBOL_ASK : SYMBOL_BID); double priceB = SymbolInfoDouble(symbolB, longA ? SYMBOL_BID : SYMBOL_ASK); MqlTradeRequest requestA = {}, requestB = {}; MqlTradeResult resultA = {}, resultB = {}; requestA.symbol = symbolA; requestA.volume = lotSize; requestA.type = longA ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; requestA.price = priceA; requestA.deviation = 10; requestA.magic = 12345; requestA.type_filling = ORDER_FILLING_FOK; requestA.action = TRADE_ACTION_DEAL; requestB.symbol = symbolB; requestB.volume = NormalizeDouble(lotSize * hedgeRatio, 2); requestB.type = longA ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; requestB.price = priceB; requestB.deviation = 10; requestB.magic = 12345; requestB.type_filling = ORDER_FILLING_FOK; requestB.action = TRADE_ACTION_DEAL; bool sentA = OrderSend(requestA, resultA); bool sentB = OrderSend(requestB, resultB); if (!sentA || resultA.retcode != TRADE_RETCODE_DONE) Print("Error al abrir operación A: ", resultA.retcode); if (!sentB || resultB.retcode != TRADE_RETCODE_DONE) Print("Error al abrir operación B: ", resultB.retcode); if (resultA.retcode == TRADE_RETCODE_DONE && resultB.retcode == TRADE_RETCODE_DONE) { inPosition = true; entryTime = TimeCurrent(); initialEntryEquity = AccountInfoDouble(ACCOUNT_EQUITY); Print("Ambas operaciones abiertas. Trade iniciado. Z = ", z); } else { if (resultA.retcode == TRADE_RETCODE_DONE) ClosePosition(symbolA); if (resultB.retcode == TRADE_RETCODE_DONE) ClosePosition(symbolB); } } void CloseTrades() { ClosePosition(symbolA); ClosePosition(symbolB); inPosition = PositionSelect(symbolA) || PositionSelect(symbolB); } void ClosePosition(string sym) { if (!PositionSelect(sym)) { Print("No hay posición abierta en ", sym, ". Posible cierre externo."); return; } if (!trade.PositionClose(sym)) { int err = GetLastError(); if (err == TRADE_RETCODE_POSITION_CLOSED) { Print("La posición ya fue cerrada por fuera en ", sym, ". Ignorando."); } else { Print("Error al cerrar posición en ", sym, ". Código: ", err); } } else { Print("Posición cerrada exitosamente en ", sym); } } double GetATR(string symbol, int period) { int handle = iATR(symbol, PERIOD_H1, period); if (handle == INVALID_HANDLE) return 0; double buffer[]; if (CopyBuffer(handle, 0, 0, 1, buffer) <= 0) return 0; return buffer[0]; } double GetFloatingProfit(string symbol) { if (!PositionSelect(symbol)) return 0.0; return PositionGetDouble(POSITION_PROFIT); } double CalculateHedgeRatio(const double &y[], const double &x[]) { int n = MathMin(ArraySize(y), ArraySize(x)); double sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; for (int i = 0; i < n; i++) { sumX += x[i]; sumY += y[i]; sumXY += x[i] * y[i]; sumXX += x[i] * x[i]; } double denom = (n * sumXX - sumX * sumX); return (denom != 0.0) ? (n * sumXY - sumX * sumY) / denom : 1.0; } double MathMean(const double &arr[]) { double sum = 0.0; for (int i = 0; i < ArraySize(arr); i++) sum += arr[i]; return sum / ArraySize(arr); } double MathStandardDeviation(const double &arr[], double mean) { double sum = 0.0; for (int i = 0; i < ArraySize(arr); i++) sum += MathPow(arr[i] - mean, 2); return MathSqrt(sum / ArraySize(arr)); }