542 lines
22 KiB
MQL5
542 lines
22 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 <Trade\Trade.mqh>
|
|
#include <Trade\PositionInfo.mqh>
|
|
#include <Trade\SymbolInfo.mqh>
|
|
|
|
//================ 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|