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>
346 lines
9.7 KiB
Plaintext
346 lines
9.7 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| RSISecretSauceStrategy.mqh |
|
|
//| Cluster-0 orchestrator: RSI leave extreme then peak/bottom entry |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef RSI_SECRET_SAUCE_STRATEGY_MQH
|
|
#define RSI_SECRET_SAUCE_STRATEGY_MQH
|
|
|
|
#include <Trade/Trade.mqh>
|
|
#include <Trade/PositionInfo.mqh>
|
|
|
|
struct RSISecretSauceOrcData
|
|
{
|
|
string actualSymbol;
|
|
bool isInitialized;
|
|
CTrade trade;
|
|
CPositionInfo positionInfo;
|
|
int rsiHandle;
|
|
int atrHandle;
|
|
double rsiBuffer[];
|
|
double atrBuffer[];
|
|
double highBuffer[];
|
|
double lowBuffer[];
|
|
bool rsiWasOverbought;
|
|
bool rsiWasOversold;
|
|
bool rsiBackInRange;
|
|
datetime lastRSIExitTime;
|
|
datetime lastRSIReentryTime;
|
|
datetime lastTradeTime;
|
|
datetime lastBarTime;
|
|
bool closeUnprofitableOnNewSignal;
|
|
};
|
|
|
|
bool RSS_UpdateIndicators(RSISecretSauceOrcData &d)
|
|
{
|
|
int rsiBarsNeeded = RSS_RSILookback + 5;
|
|
if(CopyBuffer(d.rsiHandle, 0, 0, rsiBarsNeeded, d.rsiBuffer) < rsiBarsNeeded)
|
|
return false;
|
|
if(CopyBuffer(d.atrHandle, 0, 0, 2, d.atrBuffer) < 2)
|
|
return false;
|
|
if(CopyHigh(d.actualSymbol, RSS_Timeframe, 0, RSS_SwingLookback + 5, d.highBuffer) < RSS_SwingLookback + 5)
|
|
return false;
|
|
if(CopyLow(d.actualSymbol, RSS_Timeframe, 0, RSS_SwingLookback + 5, d.lowBuffer) < RSS_SwingLookback + 5)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void RSS_UpdateRSIState(RSISecretSauceOrcData &d)
|
|
{
|
|
double rsiCurrent = d.rsiBuffer[0];
|
|
double rsiPrev = d.rsiBuffer[1];
|
|
|
|
if(rsiPrev >= RSS_RSIOverbought && rsiCurrent < RSS_RSIOverbought)
|
|
{
|
|
d.rsiWasOverbought = true;
|
|
d.rsiBackInRange = true;
|
|
d.lastRSIExitTime = TimeCurrent();
|
|
d.lastRSIReentryTime = TimeCurrent();
|
|
}
|
|
|
|
if(rsiPrev <= RSS_RSIOversold && rsiCurrent > RSS_RSIOversold)
|
|
{
|
|
d.rsiWasOversold = true;
|
|
d.rsiBackInRange = true;
|
|
d.lastRSIExitTime = TimeCurrent();
|
|
d.lastRSIReentryTime = TimeCurrent();
|
|
}
|
|
|
|
if(rsiCurrent >= RSS_RSIOverbought)
|
|
{
|
|
d.rsiWasOverbought = false;
|
|
d.rsiBackInRange = false;
|
|
}
|
|
|
|
if(rsiCurrent <= RSS_RSIOversold)
|
|
{
|
|
d.rsiWasOversold = false;
|
|
d.rsiBackInRange = false;
|
|
}
|
|
}
|
|
|
|
bool RSS_IsRSIPeak(RSISecretSauceOrcData &d)
|
|
{
|
|
if(ArraySize(d.rsiBuffer) < RSS_PeakBars + 2)
|
|
return false;
|
|
double currentRSI = d.rsiBuffer[0];
|
|
bool isPeak = true;
|
|
for(int i = 1; i <= RSS_PeakBars; i++)
|
|
{
|
|
if(d.rsiBuffer[i] >= currentRSI)
|
|
{
|
|
isPeak = false;
|
|
break;
|
|
}
|
|
}
|
|
if(d.rsiBuffer[1] >= currentRSI)
|
|
isPeak = false;
|
|
return isPeak;
|
|
}
|
|
|
|
bool RSS_IsRSIBottom(RSISecretSauceOrcData &d)
|
|
{
|
|
if(ArraySize(d.rsiBuffer) < RSS_PeakBars + 2)
|
|
return false;
|
|
double currentRSI = d.rsiBuffer[0];
|
|
bool isBottom = true;
|
|
for(int i = 1; i <= RSS_PeakBars; i++)
|
|
{
|
|
if(d.rsiBuffer[i] <= currentRSI)
|
|
{
|
|
isBottom = false;
|
|
break;
|
|
}
|
|
}
|
|
if(d.rsiBuffer[1] <= currentRSI)
|
|
isBottom = false;
|
|
return isBottom;
|
|
}
|
|
|
|
double RSS_GetSwingStopLoss(RSISecretSauceOrcData &d, double currentPrice, ENUM_POSITION_TYPE type)
|
|
{
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
double lowestLow = d.lowBuffer[0];
|
|
for(int i = 1; i < RSS_SwingLookback && i < ArraySize(d.lowBuffer); i++)
|
|
{
|
|
if(d.lowBuffer[i] < lowestLow)
|
|
lowestLow = d.lowBuffer[i];
|
|
}
|
|
return lowestLow;
|
|
}
|
|
double highestHigh = d.highBuffer[0];
|
|
for(int i = 1; i < RSS_SwingLookback && i < ArraySize(d.highBuffer); i++)
|
|
{
|
|
if(d.highBuffer[i] > highestHigh)
|
|
highestHigh = d.highBuffer[i];
|
|
}
|
|
return highestHigh;
|
|
}
|
|
|
|
bool RSS_CalculateStops(RSISecretSauceOrcData &d, double price, ENUM_POSITION_TYPE type, double &sl, double &tp)
|
|
{
|
|
double atrValue = d.atrBuffer[0];
|
|
if(atrValue <= 0)
|
|
atrValue = price * 0.01;
|
|
|
|
double slDistance = atrValue * RSS_StopLossATR;
|
|
double tpDistance = atrValue * RSS_TakeProfitATR;
|
|
|
|
int digits = (int)SymbolInfoInteger(d.actualSymbol, SYMBOL_DIGITS);
|
|
double point = SymbolInfoDouble(d.actualSymbol, SYMBOL_POINT);
|
|
int stopsLevel = (int)SymbolInfoInteger(d.actualSymbol, SYMBOL_TRADE_STOPS_LEVEL);
|
|
double minStopDistance = MathMax(stopsLevel * point, point * 10);
|
|
|
|
if(RSS_UseSwingStopLoss)
|
|
{
|
|
double swingStop = RSS_GetSwingStopLoss(d, price, type);
|
|
if(swingStop > 0)
|
|
{
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
if(swingStop < price && (price - swingStop) > minStopDistance)
|
|
slDistance = price - swingStop;
|
|
}
|
|
else
|
|
{
|
|
if(swingStop > price && (swingStop - price) > minStopDistance)
|
|
slDistance = swingStop - price;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(slDistance < minStopDistance)
|
|
slDistance = minStopDistance;
|
|
if(tpDistance < minStopDistance)
|
|
tpDistance = minStopDistance;
|
|
|
|
if(type == POSITION_TYPE_BUY)
|
|
{
|
|
sl = NormalizeDouble(price - slDistance, digits);
|
|
tp = NormalizeDouble(price + tpDistance, digits);
|
|
}
|
|
else
|
|
{
|
|
sl = NormalizeDouble(price + slDistance, digits);
|
|
tp = NormalizeDouble(price - tpDistance, digits);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool RSS_CanOpenNewPosition(RSISecretSauceOrcData &d)
|
|
{
|
|
int positionCount = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(d.positionInfo.SelectByIndex(i))
|
|
{
|
|
if(d.positionInfo.Symbol() == d.actualSymbol && d.positionInfo.Magic() == RSS_MagicNumber)
|
|
positionCount++;
|
|
}
|
|
}
|
|
if(positionCount >= RSS_MaxPositions)
|
|
{
|
|
if(!d.closeUnprofitableOnNewSignal)
|
|
return false;
|
|
if(!IsUnprofitablePositionByMagic(d.actualSymbol, (ulong)RSS_MagicNumber))
|
|
return false;
|
|
}
|
|
|
|
if(d.lastTradeTime > 0)
|
|
{
|
|
int barsSince = Bars(d.actualSymbol, RSS_Timeframe, d.lastTradeTime, TimeCurrent());
|
|
if(barsSince < RSS_MinBarsBetweenTrades)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RSS_OpenPosition(RSISecretSauceOrcData &d, ENUM_POSITION_TYPE type, const double lotSize)
|
|
{
|
|
if(!United_PrepareEntrySlot(d.trade, d.actualSymbol, (ulong)RSS_MagicNumber, d.closeUnprofitableOnNewSignal))
|
|
return;
|
|
|
|
double price = (type == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(d.actualSymbol, SYMBOL_ASK) :
|
|
SymbolInfoDouble(d.actualSymbol, SYMBOL_BID);
|
|
|
|
if(price <= 0)
|
|
return;
|
|
|
|
double sl = 0.0, tp = 0.0;
|
|
if(!RSS_CalculateStops(d, price, type, sl, tp))
|
|
return;
|
|
|
|
string comment = "RSI_Secret_" + (type == POSITION_TYPE_BUY ? "LONG" : "SHORT");
|
|
|
|
bool result = false;
|
|
if(type == POSITION_TYPE_BUY)
|
|
result = d.trade.Buy(lotSize, d.actualSymbol, 0, sl, tp, comment);
|
|
else
|
|
result = d.trade.Sell(lotSize, d.actualSymbol, 0, sl, tp, comment);
|
|
|
|
if(result)
|
|
{
|
|
d.lastTradeTime = TimeCurrent();
|
|
if(type == POSITION_TYPE_BUY)
|
|
d.rsiWasOverbought = false;
|
|
else
|
|
d.rsiWasOversold = false;
|
|
d.rsiBackInRange = false;
|
|
}
|
|
}
|
|
|
|
void RSS_CheckEntrySignals(RSISecretSauceOrcData &d, const double lotSize)
|
|
{
|
|
if(d.rsiWasOverbought && d.rsiBackInRange)
|
|
{
|
|
if(d.rsiBuffer[0] < RSS_RSIOverbought && RSS_IsRSIPeak(d))
|
|
RSS_OpenPosition(d, POSITION_TYPE_BUY, lotSize);
|
|
}
|
|
|
|
if(d.rsiWasOversold && d.rsiBackInRange)
|
|
{
|
|
if(d.rsiBuffer[0] > RSS_RSIOversold && RSS_IsRSIBottom(d))
|
|
RSS_OpenPosition(d, POSITION_TYPE_SELL, lotSize);
|
|
}
|
|
}
|
|
|
|
bool InitRSISecretSauce(RSISecretSauceOrcData &d, const string symbol)
|
|
{
|
|
d.isInitialized = false;
|
|
d.rsiHandle = INVALID_HANDLE;
|
|
d.atrHandle = INVALID_HANDLE;
|
|
d.rsiWasOverbought = false;
|
|
d.rsiWasOversold = false;
|
|
d.rsiBackInRange = false;
|
|
d.lastRSIExitTime = 0;
|
|
d.lastRSIReentryTime = 0;
|
|
d.lastTradeTime = 0;
|
|
d.lastBarTime = 0;
|
|
d.actualSymbol = symbol;
|
|
StringTrimLeft(d.actualSymbol);
|
|
StringTrimRight(d.actualSymbol);
|
|
if(StringLen(d.actualSymbol) == 0)
|
|
d.actualSymbol = _Symbol;
|
|
|
|
if(!SymbolSelect(d.actualSymbol, true))
|
|
{
|
|
Print("RSISecretSauce: symbol not available '", d.actualSymbol, "'");
|
|
return false;
|
|
}
|
|
|
|
d.rsiHandle = iRSI(d.actualSymbol, RSS_Timeframe, RSS_RSIPeriod, PRICE_CLOSE);
|
|
d.atrHandle = iATR(d.actualSymbol, RSS_Timeframe, RSS_ATRPeriod);
|
|
if(d.rsiHandle == INVALID_HANDLE || d.atrHandle == INVALID_HANDLE)
|
|
return false;
|
|
|
|
ArraySetAsSeries(d.rsiBuffer, true);
|
|
ArraySetAsSeries(d.atrBuffer, true);
|
|
ArraySetAsSeries(d.highBuffer, true);
|
|
ArraySetAsSeries(d.lowBuffer, true);
|
|
|
|
d.trade.SetExpertMagicNumber(RSS_MagicNumber);
|
|
d.trade.SetDeviationInPoints(RSS_Slippage);
|
|
d.trade.SetTypeFilling(ORDER_FILLING_FOK);
|
|
|
|
d.isInitialized = true;
|
|
return true;
|
|
}
|
|
|
|
void DeinitRSISecretSauce(RSISecretSauceOrcData &d)
|
|
{
|
|
if(d.rsiHandle != INVALID_HANDLE)
|
|
IndicatorRelease(d.rsiHandle);
|
|
if(d.atrHandle != INVALID_HANDLE)
|
|
IndicatorRelease(d.atrHandle);
|
|
d.rsiHandle = INVALID_HANDLE;
|
|
d.atrHandle = INVALID_HANDLE;
|
|
d.isInitialized = false;
|
|
}
|
|
|
|
void ProcessRSISecretSauce(RSISecretSauceOrcData &d, const double lotSize)
|
|
{
|
|
if(!d.isInitialized)
|
|
return;
|
|
|
|
int requiredBars = MathMax(RSS_RSILookback, RSS_SwingLookback) + 10;
|
|
if(Bars(d.actualSymbol, RSS_Timeframe) < requiredBars)
|
|
return;
|
|
|
|
datetime currentBarTime = iTime(d.actualSymbol, RSS_Timeframe, 0);
|
|
if(currentBarTime == d.lastBarTime)
|
|
return;
|
|
|
|
d.lastBarTime = currentBarTime;
|
|
|
|
if(!RSS_UpdateIndicators(d))
|
|
return;
|
|
|
|
RSS_UpdateRSIState(d);
|
|
|
|
if(RSS_CanOpenNewPosition(d))
|
|
RSS_CheckEntrySignals(d, lotSize);
|
|
}
|
|
|
|
#endif
|