Files
Bell-PriceActionWithEma-EA/Include/RsiMom/SignalDebug.mqh
T

579 lines
21 KiB
Plaintext
Raw Normal View History

2026-05-23 22:27:48 +07:00
//+------------------------------------------------------------------+
//| SignalDebug.mqh — đánh dấu cross RSI×WMA45: OK / SKIP + tooltip |
//+------------------------------------------------------------------+
#ifndef RSI_MOM_SIGNAL_DEBUG_MQH
#define RSI_MOM_SIGNAL_DEBUG_MQH
#include <RsiMom/PhaseEntry.mqh>
struct SignalEvalResult
{
bool signalOk;
bool tradeOk;
string summary;
string detail;
string failTag; // lý do chính: P1-mở rộng, P2-cuộn, EMA200, ATR, …
};
void SignalEval_Append(string &detail, const string part)
{
if(StringLen(detail) > 0)
detail += " | ";
detail += part;
}
void SignalEval_SetFail(string &failTag, const string tag)
{
if(StringLen(failTag) == 0 && StringLen(tag) > 0)
failTag = tag;
}
string SignalEval_FormatTooltip(const bool isBuy, const SignalEvalResult &ev)
{
const string side = isBuy ? "BUY" : "SELL";
string tip = side + " @ " + ev.summary;
if(StringLen(ev.failTag) > 0 && !ev.signalOk)
tip += "\nFail: " + ev.failTag;
tip += "\n---\n" + ev.detail;
return tip;
}
string SignalEval_ChartLabel(const SignalEvalResult &ev)
{
if(ev.tradeOk)
return "OK";
if(ev.signalOk)
return "SIG";
if(StringLen(ev.failTag) > 0)
{
string tag = ev.failTag;
StringTrimRight(tag);
StringTrimLeft(tag);
return "SKIP " + tag;
}
return "SKIP";
}
SignalEvalResult Signal_EvaluateBuyAt(const int shift, const int rates_total, const int trendN,
const double &rsi[], const double &ema9[], const double &wma[],
const double &closeArr[], const double &ema200Arr[],
const PhaseEntryConfig &phaseCfg,
const bool phaseFilterEnabled,
const bool trendFilterEnabled,
const bool atrFilterEnabled,
const bool rsiObOsFilterEnabled,
const double rsiOverbought,
const double rsiOversold,
const bool sessionFilterEnabled,
const bool atrExpAtBar,
const bool sessionAtBar,
const string sessionFailWhy = "")
{
SignalEvalResult r;
r.signalOk = false;
r.tradeOk = false;
r.summary = "";
r.detail = "";
r.failTag = "";
const bool cross = (shift + 1 < rates_total) &&
(rsi[shift + 1] <= wma[shift + 1]) && (rsi[shift] > wma[shift + 1]);
if(!cross)
{
r.summary = "no cross";
SignalEval_Append(r.detail, "RSI chưa cắt lên WMA45");
return r;
}
SignalEval_Append(r.detail, "RSI↑WMA45");
bool coreOk = true;
string phaseFail = "";
if(phaseFilterEnabled)
{
coreOk = Phase_BuyPasses(shift, rates_total, rsi, ema9, wma, phaseCfg, phaseFail);
double maxSp = 0.0;
const bool p1 = Phase_FindMaxSpreadDown(shift, phaseCfg.expandLookback, rates_total,
rsi, ema9, wma, phaseCfg.minExpandSpread, maxSp);
const int xc = Phase_CountRsiEma9Crosses(shift, phaseCfg.coilLookback, rates_total, rsi, ema9);
const bool p3 = Phase_Ema9TurningBuy(shift, phaseCfg, ema9);
const bool p4 = Phase_Wma45FlatteningBuyCfg(shift, phaseCfg, wma);
const double gap = wma[shift] - ema9[shift];
const bool p5 = Phase_Ema9NearWmaBuy(shift, phaseCfg.maxEma9WmaGap, ema9, wma);
SignalEval_Append(r.detail, p1
? StringFormat("P1:OK(sp=%.1f>=%.1f)", maxSp, phaseCfg.minExpandSpread)
: StringFormat("P1:FAIL(sp=%.1f<%.1f)", maxSp, phaseCfg.minExpandSpread));
2026-05-23 22:27:48 +07:00
SignalEval_Append(r.detail, StringFormat("P2:%s(x=%d)", xc >= phaseCfg.minRsiEma9Crosses ? "OK" : "FAIL", xc));
SignalEval_Append(r.detail, p3 ? "P3:OK" : "P3:FAIL");
SignalEval_Append(r.detail, p4 ? "P4:OK" : "P4:FAIL");
SignalEval_Append(r.detail, p5 ? StringFormat("P5:OK(gap=%.1f)", gap) : StringFormat("P5:FAIL(gap=%.1f)", gap));
if(!p1) SignalEval_SetFail(r.failTag, "P1-mở rộng");
else if(xc < phaseCfg.minRsiEma9Crosses)
SignalEval_SetFail(r.failTag, "P2-cuộn");
else if(!p3) SignalEval_SetFail(r.failTag, "P3-EMA9↑");
else if(!p4) SignalEval_SetFail(r.failTag, "P4-WMA45 phẳng");
else if(!p5) SignalEval_SetFail(r.failTag, "P5-EMA9 xa WMA45");
else if(!coreOk && StringLen(phaseFail) > 0)
SignalEval_SetFail(r.failTag, phaseFail);
}
else
{
coreOk = (ema9[shift] < wma[shift]);
SignalEval_Append(r.detail, coreOk ? "EMA9<WMA45" : "EMA9>=WMA45 FAIL");
if(!coreOk)
SignalEval_SetFail(r.failTag, "EMA9>=WMA45");
}
bool trendUp = true;
if(!trendFilterEnabled)
SignalEval_Append(r.detail, "EMA200:OFF");
else
{
for(int k = 0; k < trendN; k++)
{
const int idx = shift + k;
if(closeArr[idx] <= ema200Arr[idx])
{
trendUp = false;
break;
}
}
SignalEval_Append(r.detail, trendUp ? "EMA200:OK" : "EMA200:FAIL");
if(!trendUp)
SignalEval_SetFail(r.failTag, "EMA200");
}
SignalEval_Append(r.detail, !atrFilterEnabled ? "ATR:OFF" : (atrExpAtBar ? "ATR:OK" : "ATR:FAIL"));
if(atrFilterEnabled && !atrExpAtBar)
SignalEval_SetFail(r.failTag, "ATR co");
bool rsiOk = true;
if(!rsiObOsFilterEnabled)
SignalEval_Append(r.detail, "RSI OB/OS:OFF");
else
{
rsiOk = (rsi[shift] < rsiOverbought);
if(rsiOk)
SignalEval_Append(r.detail, StringFormat("RSI:OK(%.1f<%g)", rsi[shift], rsiOverbought));
else
{
SignalEval_Append(r.detail, StringFormat("RSI:OB %.1f>=%g", rsi[shift], rsiOverbought));
SignalEval_SetFail(r.failTag, "RSI quá mua");
}
}
const bool envBar = (!sessionFilterEnabled) || sessionAtBar;
if(!sessionFilterEnabled)
SignalEval_Append(r.detail, "Phiên:OFF");
else if(envBar)
SignalEval_Append(r.detail, "Phiên(bar):OK");
else
{
SignalEval_Append(r.detail, StringLen(sessionFailWhy) > 0
? "Phiên(bar):FAIL " + sessionFailWhy
: "Phiên(bar):FAIL");
SignalEval_SetFail(r.failTag, "Phiên");
}
r.signalOk = cross && coreOk && trendUp
&& (!atrFilterEnabled || atrExpAtBar) && rsiOk && envBar;
r.tradeOk = r.signalOk;
if(r.signalOk && r.tradeOk)
r.summary = "HỢP LỆ → vào lệnh";
else if(r.signalOk)
r.summary = "Tín hiệu OK | chờ đặt lệnh";
else
r.summary = StringLen(r.failTag) > 0
? "SKIP → " + r.failTag
: "SKIP";
return r;
}
SignalEvalResult Signal_EvaluateSellAt(const int shift, const int rates_total, const int trendN,
const double &rsi[], const double &ema9[], const double &wma[],
const double &closeArr[], const double &ema200Arr[],
const PhaseEntryConfig &phaseCfg,
const bool phaseFilterEnabled,
const bool trendFilterEnabled,
const bool atrFilterEnabled,
const bool rsiObOsFilterEnabled,
const double rsiOverbought,
const double rsiOversold,
const bool sessionFilterEnabled,
const bool atrExpAtBar,
const bool sessionAtBar,
const string sessionFailWhy = "")
{
SignalEvalResult r;
r.signalOk = false;
r.tradeOk = false;
r.summary = "";
r.detail = "";
r.failTag = "";
const bool cross = (shift + 1 < rates_total) &&
(rsi[shift + 1] >= wma[shift + 1]) && (rsi[shift] < wma[shift + 1]);
if(!cross)
{
r.summary = "no cross";
SignalEval_Append(r.detail, "RSI chưa cắt xuống WMA45");
return r;
}
SignalEval_Append(r.detail, "RSI↓WMA45");
bool coreOk = true;
string phaseFail = "";
if(phaseFilterEnabled)
{
coreOk = Phase_SellPasses(shift, rates_total, rsi, ema9, wma, phaseCfg, phaseFail);
double maxSp = 0.0;
const bool p1 = Phase_FindMaxSpreadUp(shift, phaseCfg.expandLookback, rates_total,
rsi, ema9, wma, phaseCfg.minExpandSpread, maxSp);
const int xc = Phase_CountRsiEma9Crosses(shift, phaseCfg.coilLookback, rates_total, rsi, ema9);
const bool p3 = Phase_Ema9TurningSell(shift, phaseCfg, ema9);
const bool p4 = Phase_Wma45FlatteningSellCfg(shift, phaseCfg, wma);
const double gap = ema9[shift] - wma[shift];
const bool p5 = Phase_Ema9NearWmaSell(shift, phaseCfg.maxEma9WmaGap, ema9, wma);
SignalEval_Append(r.detail, p1
? StringFormat("P1:OK(sp=%.1f>=%.1f)", maxSp, phaseCfg.minExpandSpread)
: StringFormat("P1:FAIL(sp=%.1f<%.1f)", maxSp, phaseCfg.minExpandSpread));
2026-05-23 22:27:48 +07:00
SignalEval_Append(r.detail, StringFormat("P2:%s(x=%d)", xc >= phaseCfg.minRsiEma9Crosses ? "OK" : "FAIL", xc));
SignalEval_Append(r.detail, p3 ? "P3:OK" : "P3:FAIL");
SignalEval_Append(r.detail, p4 ? "P4:OK" : "P4:FAIL");
SignalEval_Append(r.detail, p5 ? StringFormat("P5:OK(gap=%.1f)", gap) : StringFormat("P5:FAIL(gap=%.1f)", gap));
if(!p1) SignalEval_SetFail(r.failTag, "P1-mở rộng");
else if(xc < phaseCfg.minRsiEma9Crosses) SignalEval_SetFail(r.failTag, "P2-cuộn");
else if(!p3) SignalEval_SetFail(r.failTag, "P3-EMA9↓");
else if(!p4) SignalEval_SetFail(r.failTag, "P4-WMA45 phẳng");
else if(!p5) SignalEval_SetFail(r.failTag, "P5-EMA9 xa WMA45");
else if(!coreOk && StringLen(phaseFail) > 0)
SignalEval_SetFail(r.failTag, phaseFail);
}
else
{
coreOk = (ema9[shift] > wma[shift]);
SignalEval_Append(r.detail, coreOk ? "EMA9>WMA45" : "EMA9<=WMA45 FAIL");
if(!coreOk)
SignalEval_SetFail(r.failTag, "EMA9<=WMA45");
}
bool trendDown = true;
if(!trendFilterEnabled)
SignalEval_Append(r.detail, "EMA200:OFF");
else
{
for(int k = 0; k < trendN; k++)
{
const int idx = shift + k;
if(closeArr[idx] >= ema200Arr[idx])
{
trendDown = false;
break;
}
}
SignalEval_Append(r.detail, trendDown ? "EMA200:OK" : "EMA200:FAIL");
if(!trendDown)
SignalEval_SetFail(r.failTag, "EMA200");
}
SignalEval_Append(r.detail, !atrFilterEnabled ? "ATR:OFF" : (atrExpAtBar ? "ATR:OK" : "ATR:FAIL"));
if(atrFilterEnabled && !atrExpAtBar)
SignalEval_SetFail(r.failTag, "ATR co");
bool rsiOk = true;
if(!rsiObOsFilterEnabled)
SignalEval_Append(r.detail, "RSI OB/OS:OFF");
else
{
rsiOk = (rsi[shift] > rsiOversold);
if(rsiOk)
SignalEval_Append(r.detail, StringFormat("RSI:OK(%.1f>%g)", rsi[shift], rsiOversold));
else
{
SignalEval_Append(r.detail, StringFormat("RSI:OS %.1f<=%g", rsi[shift], rsiOversold));
SignalEval_SetFail(r.failTag, "RSI quá bán");
}
}
const bool envBar = (!sessionFilterEnabled) || sessionAtBar;
if(!sessionFilterEnabled)
SignalEval_Append(r.detail, "Phiên:OFF");
else if(envBar)
SignalEval_Append(r.detail, "Phiên(bar):OK");
else
{
SignalEval_Append(r.detail, StringLen(sessionFailWhy) > 0
? "Phiên(bar):FAIL " + sessionFailWhy
: "Phiên(bar):FAIL");
SignalEval_SetFail(r.failTag, "Phiên");
}
r.signalOk = cross && coreOk && trendDown && (!atrFilterEnabled || atrExpAtBar) && rsiOk && envBar;
r.tradeOk = r.signalOk;
if(r.signalOk && r.tradeOk)
r.summary = "HỢP LỆ → vào lệnh";
else if(r.signalOk)
r.summary = "Tín hiệu OK | chờ đặt lệnh";
else
r.summary = StringLen(r.failTag) > 0
? "SKIP → " + r.failTag
: "SKIP";
return r;
}
const string DBG_HOVER_LABEL = "RsiMomEA_dbg_hover";
void Signal_DebugSetTooltip(const long chartId, const string name, const string tip)
{
ObjectSetString(chartId, name, OBJPROP_TOOLTIP, tip);
}
void Signal_DebugEnsureHoverLabel(const long chartId)
{
if(ObjectFind(chartId, DBG_HOVER_LABEL) >= 0)
return;
ObjectCreate(chartId, DBG_HOVER_LABEL, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_CORNER, CORNER_LEFT_LOWER);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_XDISTANCE, 8);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_YDISTANCE, 72);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_FONTSIZE, 8);
ObjectSetString (chartId, DBG_HOVER_LABEL, OBJPROP_FONT, "Consolas");
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_BACK, true);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_BGCOLOR, clrBlack);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_SELECTABLE, false);
ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_HIDDEN, true);
ObjectSetString (chartId, DBG_HOVER_LABEL, OBJPROP_TEXT, "");
}
void Signal_DebugShowHoverHint(const long chartId, const string tip)
{
if(StringLen(tip) == 0)
return;
Signal_DebugEnsureHoverLabel(chartId);
ObjectSetString(chartId, DBG_HOVER_LABEL, OBJPROP_TEXT, tip);
ChartRedraw(chartId);
}
void Signal_DebugHideHoverHint(const long chartId)
{
if(ObjectFind(chartId, DBG_HOVER_LABEL) < 0)
return;
const string cur = ObjectGetString(chartId, DBG_HOVER_LABEL, OBJPROP_TEXT);
if(StringLen(cur) == 0)
return;
ObjectSetString(chartId, DBG_HOVER_LABEL, OBJPROP_TEXT, "");
ChartRedraw(chartId);
}
bool Signal_DebugObjectHasPrefix(const string name, const string prefix)
{
return (StringLen(name) >= StringLen(prefix) && StringSubstr(name, 0, StringLen(prefix)) == prefix);
}
bool Signal_DebugReadTipFromMark(const long chartId, const string objName, string &tip)
{
tip = "";
if(ObjectFind(chartId, objName) < 0)
return false;
tip = ObjectGetString(chartId, objName, OBJPROP_TOOLTIP);
return (StringLen(tip) > 0);
}
bool Signal_DebugFindTipAtMouse(const long chartId, const string prefix,
const int mouseX, const int mouseY, string &tip)
{
tip = "";
datetime tMouse = 0;
double pMouse = 0.0;
int subwin = 0;
if(!ChartXYToTimePrice(chartId, mouseX, mouseY, subwin, tMouse, pMouse))
return false;
const int total = ObjectsTotal(chartId, 0, -1);
for(int i = total - 1; i >= 0; i--)
{
const string name = ObjectName(chartId, i, 0, -1);
if(!Signal_DebugObjectHasPrefix(name, prefix))
continue;
const bool isHit = (StringFind(name, "_hit") >= 0);
const bool isArrow = (StringFind(name, "_ar") >= 0);
if(!isHit && !isArrow)
continue;
if(isHit)
{
const datetime t0 = (datetime)ObjectGetInteger(chartId, name, OBJPROP_TIME, 0);
const datetime t1 = (datetime)ObjectGetInteger(chartId, name, OBJPROP_TIME, 1);
const double p0 = ObjectGetDouble(chartId, name, OBJPROP_PRICE, 0);
const double p1 = ObjectGetDouble(chartId, name, OBJPROP_PRICE, 1);
if(tMouse < t0 || tMouse > t1)
continue;
if(pMouse < MathMin(p0, p1) || pMouse > MathMax(p0, p1))
continue;
return Signal_DebugReadTipFromMark(chartId, name, tip);
}
if(isArrow)
{
const datetime ta = (datetime)ObjectGetInteger(chartId, name, OBJPROP_TIME, 0);
const int halfSec = MathMax(1, PeriodSeconds(_Period) / 2);
if(MathAbs((int)(tMouse - ta)) > halfSec)
continue;
return Signal_DebugReadTipFromMark(chartId, name, tip);
}
}
const int sh = iBarShift(_Symbol, _Period, tMouse, false);
if(sh < 0)
return false;
for(int d = -1; d <= 1; d++)
{
const int s = sh + d;
if(s < 0)
continue;
const datetime bt = iTime(_Symbol, _Period, s);
const string btKey = prefix + IntegerToString((int)bt);
if(Signal_DebugReadTipFromMark(chartId, btKey + "_B_hit", tip))
return true;
if(Signal_DebugReadTipFromMark(chartId, btKey + "_S_hit", tip))
return true;
if(Signal_DebugReadTipFromMark(chartId, btKey + "_B_ar", tip))
return true;
if(Signal_DebugReadTipFromMark(chartId, btKey + "_S_ar", tip))
return true;
2026-05-23 22:27:48 +07:00
}
return false;
}
void Signal_DebugOnMouseMove(const long chartId, const string prefix, const int mouseX, const int mouseY)
{
string tip = "";
if(Signal_DebugFindTipAtMouse(chartId, prefix, mouseX, mouseY, tip))
Signal_DebugShowHoverHint(chartId, tip);
else
Signal_DebugHideHoverHint(chartId);
}
2026-05-24 01:11:43 +07:00
bool Signal_DebugMarkExists(const long chartId, const string prefix,
const datetime barTime, const bool isBuy)
{
const string arrowName = prefix + IntegerToString((int)barTime) + (isBuy ? "_B" : "_S") + "_ar";
return (ObjectFind(chartId, arrowName) >= 0);
}
void Signal_DebugPruneOlderThan(const long chartId, const string prefix, const int keepBars)
{
if(keepBars < 1)
return;
const datetime cutoff = iTime(_Symbol, _Period, keepBars);
if(cutoff == 0)
return;
const int total = ObjectsTotal(chartId, 0, -1);
for(int i = total - 1; i >= 0; i--)
{
const string name = ObjectName(chartId, i, 0, -1);
if(!Signal_DebugObjectHasPrefix(name, prefix))
continue;
const int pfxLen = StringLen(prefix);
const int und = StringFind(name, "_", pfxLen);
if(und <= pfxLen)
continue;
const datetime bt = (datetime)StringToInteger(StringSubstr(name, pfxLen, und - pfxLen));
if(bt > 0 && bt < cutoff)
ObjectDelete(chartId, name);
}
}
2026-05-23 22:27:48 +07:00
void Signal_DebugDrawMark(const long chartId, const string prefix,
const datetime barTime, const double barHigh, const double barLow,
const double markPrice, const bool isBuy, const SignalEvalResult &ev)
{
const string base = prefix + IntegerToString((int)barTime) + (isBuy ? "_B" : "_S");
const string arrowName = base + "_ar";
const string hitName = base + "_hit";
// Giữ dấu cũ — không xóa/vẽ lại khi có nến mới (hover xem lý do skip sau)
if(ObjectFind(chartId, arrowName) >= 0)
return;
const string tip = SignalEval_FormatTooltip(isBuy, ev);
color clr = clrDimGray;
int arrowCode = 251;
int arrowW = 2;
if(ev.signalOk && ev.tradeOk)
{
clr = clrLime;
arrowCode = isBuy ? 233 : 234;
arrowW = 1;
}
else if(ev.signalOk)
{
clr = clrGold;
arrowCode = isBuy ? 233 : 234;
arrowW = 1;
}
else
{
clr = clrOrangeRed;
arrowCode = 251;
arrowW = 3;
}
if(ObjectCreate(chartId, arrowName, OBJ_ARROW, 0, barTime, markPrice))
{
ObjectSetInteger(chartId, arrowName, OBJPROP_ARROWCODE, arrowCode);
ObjectSetInteger(chartId, arrowName, OBJPROP_COLOR, clr);
ObjectSetInteger(chartId, arrowName, OBJPROP_WIDTH, arrowW);
ObjectSetInteger(chartId, arrowName, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM);
ObjectSetInteger(chartId, arrowName, OBJPROP_SELECTABLE, true);
ObjectSetInteger(chartId, arrowName, OBJPROP_HIDDEN, true);
ObjectSetInteger(chartId, arrowName, OBJPROP_BACK, false);
Signal_DebugSetTooltip(chartId, arrowName, tip);
}
const datetime tEnd = barTime + (datetime)PeriodSeconds(_Period);
const double pad = MathMax(_Point * 8.0, (barHigh - barLow) * 0.08);
const double rHi = barHigh + pad;
const double rLo = barLow - pad;
const color hitFill = (color)ColorToARGB(clr, (uchar)24);
2026-05-23 22:27:48 +07:00
if(ObjectCreate(chartId, hitName, OBJ_RECTANGLE, 0, barTime, rHi, tEnd, rLo))
{
ObjectSetInteger(chartId, hitName, OBJPROP_COLOR, hitFill);
ObjectSetInteger(chartId, hitName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(chartId, hitName, OBJPROP_WIDTH, 1);
ObjectSetInteger(chartId, hitName, OBJPROP_FILL, true);
ObjectSetInteger(chartId, hitName, OBJPROP_BACK, true);
ObjectSetInteger(chartId, hitName, OBJPROP_SELECTABLE, true);
ObjectSetInteger(chartId, hitName, OBJPROP_HIDDEN, true);
Signal_DebugSetTooltip(chartId, hitName, tip);
}
}
void Signal_DebugConfigureChart(const long chartId)
{
ChartSetInteger(chartId, CHART_SHOW_OBJECT_DESCR, false);
ChartSetInteger(chartId, CHART_EVENT_MOUSE_MOVE, true);
Signal_DebugEnsureHoverLabel(chartId);
}
#endif