Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
299 lines
9.2 KiB
Plaintext
299 lines
9.2 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| RSICrossOverReversalStrategy.mqh |
|
|
//+------------------------------------------------------------------+
|
|
|
|
void WeekDays_Init()
|
|
{
|
|
rcData.WeekDays[0] = RC_Sunday;
|
|
rcData.WeekDays[1] = RC_Monday;
|
|
rcData.WeekDays[2] = RC_Tuesday;
|
|
rcData.WeekDays[3] = RC_Wednesday;
|
|
rcData.WeekDays[4] = RC_Thursday;
|
|
rcData.WeekDays[5] = RC_Friday;
|
|
rcData.WeekDays[6] = RC_Saturday;
|
|
}
|
|
|
|
bool WeekDays_Check(datetime aTime)
|
|
{
|
|
MqlDateTime stm;
|
|
TimeToStruct(aTime, stm);
|
|
return(rcData.WeekDays[stm.day_of_week]);
|
|
}
|
|
|
|
bool RC_HourInWindow(const int h, const int beginRaw, const int endRaw)
|
|
{
|
|
const int b = beginRaw % 24;
|
|
const int e = endRaw % 24;
|
|
if(b == e)
|
|
return false;
|
|
if(b < e)
|
|
return (h >= b && h < e);
|
|
return (h >= b || h < e);
|
|
}
|
|
|
|
bool RC_TradingHoursAllow(const int currentHour)
|
|
{
|
|
return RC_HourInWindow(currentHour, RC_tradingHourOneBegin, RC_tradingHourOneEnd)
|
|
|| RC_HourInWindow(currentHour, RC_tradingHourTwoBegin, RC_tradingHourTwoEnd);
|
|
}
|
|
|
|
int TimeHour(datetime when = 0)
|
|
{
|
|
if(when == 0) when = TimeCurrent();
|
|
MqlDateTime dt;
|
|
TimeToStruct(when, dt);
|
|
return dt.hour;
|
|
}
|
|
|
|
double RC_NormalizeLot(const string sym, const double lots)
|
|
{
|
|
const double mn = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
|
|
const double mx = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
|
|
double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
|
|
if(step <= 0.0)
|
|
step = 0.01;
|
|
double v = MathMax(lots, mn);
|
|
v = MathMin(v, mx);
|
|
return NormalizeDouble(MathFloor(v / step + 0.5) * step, 2);
|
|
}
|
|
|
|
// Threshold <= 0 disables that leg (optimizer must not treat 0 as "always strong").
|
|
bool RC_IsTrendStrong(const double emaSlope, const double priceToEmaDistance)
|
|
{
|
|
if(!RC_UseTrendStrengthFilter)
|
|
return false;
|
|
const bool slopeStrong = (RC_emaSlopeThreshold > 0.0)
|
|
&& (MathAbs(emaSlope) > RC_emaSlopeThreshold);
|
|
const bool distanceStrong = (RC_emaDistanceThreshold > 0.0)
|
|
&& (MathAbs(priceToEmaDistance) > RC_emaDistanceThreshold);
|
|
return slopeStrong || distanceStrong;
|
|
}
|
|
|
|
bool InitRSICrossOverReversal(string symbol)
|
|
{
|
|
WeekDays_Init();
|
|
|
|
rcData.symbol = symbol;
|
|
rcData.previousRSIDef = 0;
|
|
rcData.lastTradeTime = 0;
|
|
rcData.bartime = 0;
|
|
rcData.lastBarTime = 0;
|
|
|
|
// Check if symbol exists
|
|
if(!SymbolSelect(symbol, true))
|
|
{
|
|
Print("RSICrossOverReversal: Symbol '", symbol, "' not available in Market Watch. Please add it to Market Watch or check symbol name.");
|
|
return false;
|
|
}
|
|
|
|
Sleep(100); // Wait for symbol to be ready
|
|
|
|
rcData.rsiHandle = iRSI(symbol, RC_TimeFrame1, RC_rsiPeriod, PRICE_CLOSE);
|
|
if(rcData.rsiHandle == INVALID_HANDLE)
|
|
{
|
|
Print("RSICrossOverReversal: Error creating RSI handle for '", symbol, "'");
|
|
return false;
|
|
}
|
|
|
|
rcData.emaHandle = iMA(symbol, RC_TimeFrame2, RC_emaPeriod, 0, MODE_EMA, PRICE_CLOSE);
|
|
if(rcData.emaHandle == INVALID_HANDLE)
|
|
{
|
|
Print("RSICrossOverReversal: Error creating EMA handle for '", symbol, "'");
|
|
return false;
|
|
}
|
|
|
|
rcData.trade.SetExpertMagicNumber(RC_MagicNumber);
|
|
rcData.trade.SetDeviationInPoints(RC_slippage);
|
|
rcData.trade.SetTypeFillingBySymbol(symbol);
|
|
rcData.isInitialized = true;
|
|
Print("RSICrossOverReversal: Successfully initialized for symbol '", symbol, "'");
|
|
return true;
|
|
}
|
|
|
|
void DeinitRSICrossOverReversal()
|
|
{
|
|
if(rcData.rsiHandle != INVALID_HANDLE)
|
|
IndicatorRelease(rcData.rsiHandle);
|
|
if(rcData.emaHandle != INVALID_HANDLE)
|
|
IndicatorRelease(rcData.emaHandle);
|
|
}
|
|
|
|
void Close_Position_MN(ulong magicNumber)
|
|
{
|
|
ClosePositionByMagic(rcData.trade, rcData.symbol, (int)magicNumber);
|
|
}
|
|
|
|
void ApplyTrailingStop()
|
|
{
|
|
if(!PositionSelectByMagic(rcData.symbol, RC_MagicNumber))
|
|
return;
|
|
|
|
ulong PositionTicket = PositionGetInteger(POSITION_TICKET);
|
|
ENUM_POSITION_TYPE trade_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
string symbol = rcData.symbol;
|
|
|
|
double POINT = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
int DIGIT = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
|
|
if(trade_type == POSITION_TYPE_BUY)
|
|
{
|
|
double Bid = NormalizeDouble(SymbolInfoDouble(symbol, SYMBOL_BID), DIGIT);
|
|
|
|
if(Bid - PositionGetDouble(POSITION_PRICE_OPEN) > NormalizeDouble(POINT * RC_TrailingStop, DIGIT))
|
|
{
|
|
if(PositionGetDouble(POSITION_SL) < NormalizeDouble(Bid - POINT * RC_TrailingStop, DIGIT))
|
|
{
|
|
ModifyPositionByMagic(rcData.trade, symbol, RC_MagicNumber,
|
|
NormalizeDouble(Bid - POINT * RC_TrailingStop, DIGIT),
|
|
PositionGetDouble(POSITION_TP));
|
|
}
|
|
}
|
|
}
|
|
else if(trade_type == POSITION_TYPE_SELL)
|
|
{
|
|
double Ask = NormalizeDouble(SymbolInfoDouble(symbol, SYMBOL_ASK), DIGIT);
|
|
|
|
if((PositionGetDouble(POSITION_PRICE_OPEN) - Ask) > NormalizeDouble(POINT * RC_TrailingStop, DIGIT))
|
|
{
|
|
if((PositionGetDouble(POSITION_SL) > NormalizeDouble(Ask + POINT * RC_TrailingStop, DIGIT)) ||
|
|
(PositionGetDouble(POSITION_SL) == 0))
|
|
{
|
|
ModifyPositionByMagic(rcData.trade, symbol, RC_MagicNumber,
|
|
NormalizeDouble(Ask + POINT * RC_TrailingStop, DIGIT),
|
|
PositionGetDouble(POSITION_TP));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessRSICrossOverReversal(string symbol)
|
|
{
|
|
// Skip if not initialized (symbol not available)
|
|
if(!rcData.isInitialized)
|
|
return;
|
|
|
|
rcData.symbol = symbol; // Update symbol in case it changed
|
|
const datetime barTime = iTime(rcData.symbol, RC_BarTimeFrame, 0);
|
|
if(barTime == 0)
|
|
return;
|
|
if(rcData.bartime == barTime)
|
|
return;
|
|
rcData.bartime = barTime;
|
|
|
|
double rsi[];
|
|
if(CopyBuffer(rcData.rsiHandle, 0, 0, 2, rsi) <= 0)
|
|
return;
|
|
|
|
double ema[];
|
|
if(CopyBuffer(rcData.emaHandle, 0, 0, 2, ema) <= 0)
|
|
return;
|
|
|
|
datetime currentTime = TimeCurrent();
|
|
int currentHour = TimeHour(TimeCurrent());
|
|
|
|
if(!WeekDays_Check(TimeTradeServer()))
|
|
{
|
|
Close_Position_MN(RC_MagicNumber);
|
|
return;
|
|
}
|
|
|
|
if(!RC_TradingHoursAllow(currentHour))
|
|
{
|
|
Close_Position_MN(RC_MagicNumber);
|
|
return;
|
|
}
|
|
|
|
bool hasPosition = PositionExistsByMagic(rcData.symbol, RC_MagicNumber);
|
|
|
|
double currentRSI = rsi[0];
|
|
double previousRSI = rsi[1];
|
|
|
|
if(rcData.previousRSIDef == 0)
|
|
{
|
|
rcData.previousRSIDef = currentRSI;
|
|
return;
|
|
}
|
|
|
|
double currentEMA = ema[0];
|
|
double previousEMA = ema[1];
|
|
|
|
double emaSlope = (currentEMA - previousEMA) * 100;
|
|
const double closeCurr = iClose(rcData.symbol, RC_TimeFrame1, 0);
|
|
double priceToEmaDistance = (closeCurr - currentEMA) * 10;
|
|
|
|
bool isBuyPosition = false;
|
|
bool isSellPosition = false;
|
|
if(hasPosition)
|
|
{
|
|
if(PositionSelectByMagic(rcData.symbol, RC_MagicNumber))
|
|
{
|
|
ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
isBuyPosition = true;
|
|
else if(positionType == POSITION_TYPE_SELL)
|
|
isSellPosition = true;
|
|
}
|
|
}
|
|
|
|
ApplyTrailingStop();
|
|
|
|
bool cooldownPassed = (currentTime - rcData.lastTradeTime) >= RC_cooldownSeconds;
|
|
const bool isTrendStrong = RC_IsTrendStrong(emaSlope, priceToEmaDistance);
|
|
|
|
if(isBuyPosition && currentRSI > RC_exitBuyRSI)
|
|
{
|
|
Close_Position_MN(RC_MagicNumber);
|
|
rcData.lastTradeTime = currentTime;
|
|
}
|
|
|
|
if(isSellPosition && currentRSI < RC_exitSellRSI)
|
|
{
|
|
Close_Position_MN(RC_MagicNumber);
|
|
rcData.lastTradeTime = currentTime;
|
|
}
|
|
|
|
if(isTrendStrong)
|
|
{
|
|
Close_Position_MN(RC_MagicNumber);
|
|
rcData.lastTradeTime = currentTime;
|
|
}
|
|
|
|
hasPosition = PositionExistsByMagic(rcData.symbol, RC_MagicNumber);
|
|
isBuyPosition = false;
|
|
isSellPosition = false;
|
|
if(hasPosition && PositionSelectByMagic(rcData.symbol, RC_MagicNumber))
|
|
{
|
|
const ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
if(positionType == POSITION_TYPE_BUY)
|
|
isBuyPosition = true;
|
|
else if(positionType == POSITION_TYPE_SELL)
|
|
isSellPosition = true;
|
|
}
|
|
|
|
const double lots = RC_NormalizeLot(rcData.symbol, g_RC_LotSize);
|
|
if(lots > 0.0 && !isTrendStrong && cooldownPassed
|
|
&& United_PrepareEntrySlot(rcData.trade, rcData.symbol, RC_MagicNumber, RC_CloseUnprofitableOnNewSignal))
|
|
{
|
|
if(currentRSI < RC_overboughtLevel - RC_entryRSISellSpread
|
|
&& rcData.previousRSIDef >= RC_overboughtLevel
|
|
&& !isSellPosition)
|
|
{
|
|
rcData.trade.SetExpertMagicNumber(RC_MagicNumber);
|
|
if(rcData.trade.Sell(lots, rcData.symbol, 0.0, 0.0, 0.0, "Sell Order"))
|
|
rcData.lastTradeTime = currentTime;
|
|
}
|
|
else if(currentRSI > RC_oversoldLevel + RC_entryRSIBuySpread
|
|
&& rcData.previousRSIDef <= RC_oversoldLevel
|
|
&& !isBuyPosition)
|
|
{
|
|
rcData.trade.SetExpertMagicNumber(RC_MagicNumber);
|
|
if(rcData.trade.Buy(lots, rcData.symbol, 0.0, 0.0, 0.0, "Buy Order"))
|
|
rcData.lastTradeTime = currentTime;
|
|
}
|
|
}
|
|
|
|
rcData.previousRSIDef = currentRSI;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|