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>
390 lines
12 KiB
Plaintext
390 lines
12 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| RSIConsolidationStrategy.mqh |
|
|
//| Ported from cluster-0/RSIConsolidation/RSIConsolidation.mq5 |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef RSI_CONSOLIDATION_STRATEGY_MQH
|
|
#define RSI_CONSOLIDATION_STRATEGY_MQH
|
|
|
|
struct RSIConsolidationData
|
|
{
|
|
string symbol;
|
|
bool isInitialized;
|
|
CTrade trade;
|
|
ENUM_TIMEFRAMES signalTF;
|
|
bool entryOnNewBarOnly;
|
|
int adxPeriod;
|
|
double adxMax;
|
|
bool useATRRatioFilter;
|
|
int atrPeriod;
|
|
int atrSmaPeriod;
|
|
double atrRatioMax;
|
|
bool useFlatEMAFilter;
|
|
int emaFast;
|
|
int emaSlow;
|
|
double emaSeparationMaxPct;
|
|
int rsiPeriod;
|
|
ENUM_APPLIED_PRICE rsiPrice;
|
|
double rsiOversold;
|
|
double rsiOverbought;
|
|
bool useRSIMeanExit;
|
|
double rsiExitLong;
|
|
double rsiExitShort;
|
|
double slAtrMult;
|
|
double tpAtrMult;
|
|
int maxBarsInTrade;
|
|
ulong magic;
|
|
int slippage;
|
|
int maxSpreadPoints;
|
|
bool closeUnprofitableOnNewSignal;
|
|
int h_rsi;
|
|
int h_adx;
|
|
int h_atr;
|
|
int h_ema_fast;
|
|
int h_ema_slow;
|
|
datetime lastBar;
|
|
};
|
|
|
|
bool RCO_Copy1(const int handle, double &v)
|
|
{
|
|
double b[];
|
|
ArraySetAsSeries(b, true);
|
|
if(CopyBuffer(handle, 0, 0, 1, b) < 1)
|
|
return false;
|
|
v = b[0];
|
|
return true;
|
|
}
|
|
|
|
bool RCO_RsiBuffers(RSIConsolidationData &d, double &cur, double &prev, double &twoAgo)
|
|
{
|
|
double b[];
|
|
ArraySetAsSeries(b, true);
|
|
if(CopyBuffer(d.h_rsi, 0, 0, 3, b) < 3)
|
|
return false;
|
|
cur = b[0];
|
|
prev = b[1];
|
|
twoAgo = b[2];
|
|
return true;
|
|
}
|
|
|
|
double RCO_NormalizeVolume(const string sym, double vol)
|
|
{
|
|
double minLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
|
|
double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
|
|
if(step > 0.0)
|
|
vol = MathFloor(vol / step) * step;
|
|
if(vol < minLot)
|
|
vol = minLot;
|
|
if(vol > maxLot)
|
|
vol = maxLot;
|
|
return vol;
|
|
}
|
|
|
|
int RCO_CurrentSpreadPoints(const string sym)
|
|
{
|
|
long spread = 0;
|
|
if(!SymbolInfoInteger(sym, SYMBOL_SPREAD, spread))
|
|
return 999999;
|
|
return (int)spread;
|
|
}
|
|
|
|
double RCO_MinStopsDistancePrice(const string sym)
|
|
{
|
|
long lvl = 0;
|
|
if(!SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL, lvl))
|
|
return 0;
|
|
double pt = SymbolInfoDouble(sym, SYMBOL_POINT);
|
|
if(pt <= 0)
|
|
return 0;
|
|
return (double)lvl * pt;
|
|
}
|
|
|
|
bool RCO_RegimeIsConsolidation(RSIConsolidationData &d)
|
|
{
|
|
double adx = 0;
|
|
if(!RCO_Copy1(d.h_adx, adx))
|
|
return false;
|
|
if(adx >= d.adxMax)
|
|
return false;
|
|
|
|
if(d.useATRRatioFilter)
|
|
{
|
|
double atrArr[];
|
|
ArraySetAsSeries(atrArr, true);
|
|
if(CopyBuffer(d.h_atr, 0, 0, d.atrSmaPeriod + 1, atrArr) < d.atrSmaPeriod + 1)
|
|
return false;
|
|
double sum = 0;
|
|
for(int i = 1; i <= d.atrSmaPeriod; i++)
|
|
sum += atrArr[i];
|
|
double smaAtr = sum / (double)d.atrSmaPeriod;
|
|
if(smaAtr <= 0.0)
|
|
return false;
|
|
double ratio = atrArr[0] / smaAtr;
|
|
if(ratio > d.atrRatioMax)
|
|
return false;
|
|
}
|
|
|
|
if(d.useFlatEMAFilter)
|
|
{
|
|
double ef[], es[];
|
|
ArraySetAsSeries(ef, true);
|
|
ArraySetAsSeries(es, true);
|
|
if(CopyBuffer(d.h_ema_fast, 0, 0, 1, ef) < 1)
|
|
return false;
|
|
if(CopyBuffer(d.h_ema_slow, 0, 0, 1, es) < 1)
|
|
return false;
|
|
double c = SymbolInfoDouble(d.symbol, SYMBOL_BID);
|
|
if(c <= 0)
|
|
return false;
|
|
double sep = MathAbs(ef[0] - es[0]) / c * 100.0;
|
|
if(sep > d.emaSeparationMaxPct)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool RCO_EntryBuyCross(RSIConsolidationData &d, const double twoAgo, const double prev)
|
|
{
|
|
return (twoAgo <= d.rsiOversold && prev > d.rsiOversold);
|
|
}
|
|
|
|
bool RCO_EntrySellCross(RSIConsolidationData &d, const double twoAgo, const double prev)
|
|
{
|
|
return (twoAgo >= d.rsiOverbought && prev < d.rsiOverbought);
|
|
}
|
|
|
|
void RCO_TryCloseByRSI(RSIConsolidationData &d, const ENUM_POSITION_TYPE typ, const double rsi)
|
|
{
|
|
ulong tk = GetPositionTicketByMagic(d.symbol, d.magic);
|
|
if(tk == 0 || !PositionSelectByTicketSymbolAndMagic(tk, d.symbol, d.magic))
|
|
return;
|
|
if(!d.useRSIMeanExit)
|
|
return;
|
|
if(typ == POSITION_TYPE_BUY && rsi >= d.rsiExitLong)
|
|
d.trade.PositionClose(tk);
|
|
else if(typ == POSITION_TYPE_SELL && rsi <= d.rsiExitShort)
|
|
d.trade.PositionClose(tk);
|
|
}
|
|
|
|
void RCO_ManageOpenPosition(RSIConsolidationData &d, const double rsi)
|
|
{
|
|
ulong tk = GetPositionTicketByMagic(d.symbol, d.magic);
|
|
if(tk == 0 || !PositionSelectByTicketSymbolAndMagic(tk, d.symbol, d.magic))
|
|
return;
|
|
ENUM_POSITION_TYPE typ = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
datetime openT = (datetime)PositionGetInteger(POSITION_TIME);
|
|
int barsAgo = iBarShift(d.symbol, d.signalTF, openT, false);
|
|
if(barsAgo >= 0 && barsAgo >= d.maxBarsInTrade)
|
|
{
|
|
d.trade.PositionClose(tk);
|
|
return;
|
|
}
|
|
RCO_TryCloseByRSI(d, typ, rsi);
|
|
}
|
|
|
|
bool InitRSIConsolidation(RSIConsolidationData &d,
|
|
const string inpSymbol,
|
|
const ENUM_TIMEFRAMES signalTF,
|
|
const bool entryOnNewBarOnly,
|
|
const int adxPeriod,
|
|
const double adxMax,
|
|
const bool useATRRatioFilter,
|
|
const int atrPeriod,
|
|
const int atrSmaPeriod,
|
|
const double atrRatioMax,
|
|
const bool useFlatEMAFilter,
|
|
const int emaFast,
|
|
const int emaSlow,
|
|
const double emaSeparationMaxPct,
|
|
const int rsiPeriod,
|
|
const ENUM_APPLIED_PRICE rsiPrice,
|
|
const double rsiOversold,
|
|
const double rsiOverbought,
|
|
const bool useRSIMeanExit,
|
|
const double rsiExitLong,
|
|
const double rsiExitShort,
|
|
const double slAtrMult,
|
|
const double tpAtrMult,
|
|
const int maxBarsInTrade,
|
|
const ulong magic,
|
|
const int slippage,
|
|
const int maxSpreadPoints)
|
|
{
|
|
d.isInitialized = false;
|
|
d.symbol = inpSymbol;
|
|
StringTrimLeft(d.symbol);
|
|
StringTrimRight(d.symbol);
|
|
if(StringLen(d.symbol) == 0)
|
|
d.symbol = _Symbol;
|
|
|
|
d.signalTF = signalTF;
|
|
d.entryOnNewBarOnly = entryOnNewBarOnly;
|
|
d.adxPeriod = adxPeriod;
|
|
d.adxMax = adxMax;
|
|
d.useATRRatioFilter = useATRRatioFilter;
|
|
d.atrPeriod = atrPeriod;
|
|
d.atrSmaPeriod = atrSmaPeriod;
|
|
d.atrRatioMax = atrRatioMax;
|
|
d.useFlatEMAFilter = useFlatEMAFilter;
|
|
d.emaFast = emaFast;
|
|
d.emaSlow = emaSlow;
|
|
d.emaSeparationMaxPct = emaSeparationMaxPct;
|
|
d.rsiPeriod = rsiPeriod;
|
|
d.rsiPrice = rsiPrice;
|
|
d.rsiOversold = rsiOversold;
|
|
d.rsiOverbought = rsiOverbought;
|
|
d.useRSIMeanExit = useRSIMeanExit;
|
|
d.rsiExitLong = rsiExitLong;
|
|
d.rsiExitShort = rsiExitShort;
|
|
d.slAtrMult = slAtrMult;
|
|
d.tpAtrMult = tpAtrMult;
|
|
d.maxBarsInTrade = maxBarsInTrade;
|
|
d.magic = magic;
|
|
d.slippage = slippage;
|
|
d.maxSpreadPoints = maxSpreadPoints;
|
|
d.lastBar = 0;
|
|
d.h_rsi = INVALID_HANDLE;
|
|
d.h_adx = INVALID_HANDLE;
|
|
d.h_atr = INVALID_HANDLE;
|
|
d.h_ema_fast = INVALID_HANDLE;
|
|
d.h_ema_slow = INVALID_HANDLE;
|
|
d.isInitialized = false;
|
|
|
|
if(!SymbolSelect(d.symbol, true))
|
|
{
|
|
Print("RSIConsolidation: SymbolSelect failed: ", d.symbol);
|
|
return false;
|
|
}
|
|
|
|
d.trade.SetExpertMagicNumber((long)d.magic);
|
|
d.trade.SetDeviationInPoints(d.slippage);
|
|
d.trade.SetTypeFillingBySymbol(d.symbol);
|
|
|
|
d.h_rsi = iRSI(d.symbol, d.signalTF, d.rsiPeriod, d.rsiPrice);
|
|
d.h_adx = iADX(d.symbol, d.signalTF, d.adxPeriod);
|
|
d.h_atr = iATR(d.symbol, d.signalTF, d.atrPeriod);
|
|
d.h_ema_fast = iMA(d.symbol, d.signalTF, d.emaFast, 0, MODE_EMA, PRICE_CLOSE);
|
|
d.h_ema_slow = iMA(d.symbol, d.signalTF, d.emaSlow, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
if(d.h_rsi == INVALID_HANDLE || d.h_adx == INVALID_HANDLE || d.h_atr == INVALID_HANDLE
|
|
|| d.h_ema_fast == INVALID_HANDLE || d.h_ema_slow == INVALID_HANDLE)
|
|
{
|
|
Print("RSIConsolidation: indicator init failed");
|
|
DeinitRSIConsolidation(d);
|
|
return false;
|
|
}
|
|
|
|
d.isInitialized = true;
|
|
Print("RSIConsolidation: symbol=", d.symbol, " TF=", EnumToString(d.signalTF));
|
|
return true;
|
|
}
|
|
|
|
void DeinitRSIConsolidation(RSIConsolidationData &d)
|
|
{
|
|
if(d.h_rsi != INVALID_HANDLE)
|
|
IndicatorRelease(d.h_rsi);
|
|
if(d.h_adx != INVALID_HANDLE)
|
|
IndicatorRelease(d.h_adx);
|
|
if(d.h_atr != INVALID_HANDLE)
|
|
IndicatorRelease(d.h_atr);
|
|
if(d.h_ema_fast != INVALID_HANDLE)
|
|
IndicatorRelease(d.h_ema_fast);
|
|
if(d.h_ema_slow != INVALID_HANDLE)
|
|
IndicatorRelease(d.h_ema_slow);
|
|
d.h_rsi = INVALID_HANDLE;
|
|
d.h_adx = INVALID_HANDLE;
|
|
d.h_atr = INVALID_HANDLE;
|
|
d.h_ema_fast = INVALID_HANDLE;
|
|
d.h_ema_slow = INVALID_HANDLE;
|
|
d.isInitialized = false;
|
|
}
|
|
|
|
bool RCO_EnoughHistory(RSIConsolidationData &d)
|
|
{
|
|
int need = MathMax(d.rsiPeriod + 3, MathMax(d.adxPeriod + 2, d.atrSmaPeriod + 3));
|
|
if(Bars(d.symbol, d.signalTF) < need)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void ProcessRSIConsolidation(RSIConsolidationData &d, const double lots)
|
|
{
|
|
if(!d.isInitialized)
|
|
return;
|
|
|
|
if(!RCO_EnoughHistory(d))
|
|
return;
|
|
|
|
if(d.maxSpreadPoints > 0 && RCO_CurrentSpreadPoints(d.symbol) > d.maxSpreadPoints)
|
|
return;
|
|
|
|
double rsi, rsiPrev, rsi2;
|
|
if(!RCO_RsiBuffers(d, rsi, rsiPrev, rsi2))
|
|
return;
|
|
|
|
datetime barTime = iTime(d.symbol, d.signalTF, 0);
|
|
bool isNew = (barTime != d.lastBar);
|
|
|
|
if(PositionExistsByMagic(d.symbol, d.magic))
|
|
{
|
|
RCO_ManageOpenPosition(d, rsi);
|
|
if(isNew)
|
|
d.lastBar = barTime;
|
|
if(!d.closeUnprofitableOnNewSignal)
|
|
return;
|
|
}
|
|
|
|
if(d.entryOnNewBarOnly && !isNew)
|
|
return;
|
|
|
|
d.lastBar = barTime;
|
|
|
|
if(!RCO_RegimeIsConsolidation(d))
|
|
return;
|
|
|
|
double atrArr[];
|
|
ArraySetAsSeries(atrArr, true);
|
|
if(CopyBuffer(d.h_atr, 0, 0, 1, atrArr) < 1)
|
|
return;
|
|
double atr = atrArr[0];
|
|
int dig = (int)SymbolInfoInteger(d.symbol, SYMBOL_DIGITS);
|
|
|
|
double slDist = atr * d.slAtrMult;
|
|
double tpDist = atr * d.tpAtrMult;
|
|
double minD = RCO_MinStopsDistancePrice(d.symbol);
|
|
if(slDist < minD)
|
|
slDist = minD;
|
|
if(tpDist < minD)
|
|
tpDist = minD;
|
|
|
|
double vol = RCO_NormalizeVolume(d.symbol, lots);
|
|
|
|
if(RCO_EntryBuyCross(d, rsi2, rsiPrev))
|
|
{
|
|
if(!United_MayOpenNewEntry(d.symbol, d.magic, true, d.trade, d.closeUnprofitableOnNewSignal))
|
|
return;
|
|
double ask = SymbolInfoDouble(d.symbol, SYMBOL_ASK);
|
|
double sl = ask - slDist;
|
|
double tp = ask + tpDist;
|
|
sl = NormalizeDouble(sl, dig);
|
|
tp = NormalizeDouble(tp, dig);
|
|
if(!d.trade.Buy(vol, d.symbol, ask, sl, tp, "RSIConsolidation BUY"))
|
|
Print("RSIConsolidation BUY failed | retcode=", d.trade.ResultRetcode(), " ", d.trade.ResultRetcodeDescription());
|
|
}
|
|
else if(RCO_EntrySellCross(d, rsi2, rsiPrev))
|
|
{
|
|
if(!United_MayOpenNewEntry(d.symbol, d.magic, false, d.trade, d.closeUnprofitableOnNewSignal))
|
|
return;
|
|
double bid = SymbolInfoDouble(d.symbol, SYMBOL_BID);
|
|
double sl = bid + slDist;
|
|
double tp = bid - tpDist;
|
|
sl = NormalizeDouble(sl, dig);
|
|
tp = NormalizeDouble(tp, dig);
|
|
if(!d.trade.Sell(vol, d.symbol, bid, sl, tp, "RSIConsolidation SELL"))
|
|
Print("RSIConsolidation SELL failed | retcode=", d.trade.ResultRetcode(), " ", d.trade.ResultRetcodeDescription());
|
|
}
|
|
}
|
|
|
|
#endif // RSI_CONSOLIDATION_STRATEGY_MQH
|