Files
Bell-PriceActionWithEma-EA/Experts/RSIForceStateEA/Visualizer.mqh
T
2026-05-09 23:16:50 +07:00

367 lines
13 KiB
Plaintext

#ifndef RSI_FORCE_STATE_EA__VISUALIZER_MQH
#define RSI_FORCE_STATE_EA__VISUALIZER_MQH
// ============================================================
// Visual layer for RSIForceStateEA.
// Renders:
// - Top-left dashboard (trend, state, key indicator values).
// - Bottom-left stats panel (totals, TP, SL, net P/L).
// - TradingView-style Entry/SL/TP horizontal lines with labels.
// - Optionally attaches EMA200/RSI/EMA9/WMA45 to the chart.
// All visual objects share the prefix "RSIForce_" and are removed
// in OnDeinit via RemoveAllVisuals().
// ============================================================
#define VIS_PREFIX "RSIForce_"
#define VIS_LBL_DASH_PREFIX VIS_PREFIX "DASH_"
#define VIS_LBL_STATS_PREFIX VIS_PREFIX "STATS_"
#define VIS_OBJ_LEVEL_PREFIX VIS_PREFIX "LV_"
// Aggregated stats computed from the deal history.
struct VisualTradeStats
{
int totalClosed; // unique closed positions (any reason)
int tpHits; // close deals with reason TP
int slHits; // close deals with reason SL
int otherCloses; // manual / expert / partial
double netPL; // sum of profit on close deals
};
// ------------------------------------------------------------
// Generic object helpers
// ------------------------------------------------------------
void EnsureLabel(const string objName, const int xDist, const int yDist,
const ENUM_BASE_CORNER corner, const color clr,
const string text, const int fontSize = 9)
{
if (ObjectFind(0, objName) < 0)
{
ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDist);
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDist);
ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
ObjectSetString (0, objName, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, objName, OBJPROP_BACK, false);
ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
}
ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
ObjectSetString (0, objName, OBJPROP_TEXT, text);
}
void EnsureHLine(const string objName, const double price, const color clr,
const ENUM_LINE_STYLE style, const int width, const string text)
{
if (ObjectFind(0, objName) < 0)
{
ObjectCreate(0, objName, OBJ_HLINE, 0, 0, price);
ObjectSetInteger(0, objName, OBJPROP_BACK, true);
ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
}
ObjectSetDouble (0, objName, OBJPROP_PRICE, price);
ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
ObjectSetInteger(0, objName, OBJPROP_STYLE, style);
ObjectSetInteger(0, objName, OBJPROP_WIDTH, width);
ObjectSetString (0, objName, OBJPROP_TEXT, text);
}
color TrendColor(const TrendDirection trend)
{
if (trend == TREND_UP) return InpColorTrendUp;
if (trend == TREND_DOWN) return InpColorTrendDown;
return InpColorTrendNone;
}
string TrendLabel(const TrendDirection trend)
{
if (trend == TREND_UP) return "UP";
if (trend == TREND_DOWN) return "DOWN";
return "NONE";
}
// ------------------------------------------------------------
// Top-left dashboard
// ------------------------------------------------------------
void DrawDashboardPanel(const TrendDirection trendNow)
{
if (!InpShowDashboard) return;
const int xLeft = 12;
int y = 12;
const int rowH = 18;
EnsureLabel(VIS_LBL_DASH_PREFIX "title", xLeft, y, CORNER_LEFT_UPPER,
clrWhite, "RSIForceStateEA", 11); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "trend", xLeft, y, CORNER_LEFT_UPPER,
TrendColor(trendNow),
"Trend : " + TrendLabel(trendNow)); y += rowH;
color stateClr = (g_State == STATE_IN_TRADE) ? clrLime
: (g_State == STATE_PENDING_ORDER) ? clrOrange
: (g_State == STATE_WATCHING) ? clrYellow
: clrSilver;
EnsureLabel(VIS_LBL_DASH_PREFIX "state", xLeft, y, CORNER_LEFT_UPPER,
stateClr,
"State : " + EnumToString(g_State)); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "rsi", xLeft, y, CORNER_LEFT_UPPER,
clrAqua,
StringFormat("RSI : %6.2f EMA9 : %6.2f WMA45: %6.2f",
g_RSI[1], g_EMA9[1], g_WMA45[1])); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "ema200", xLeft, y, CORNER_LEFT_UPPER,
InpColorEntry,
StringFormat("EMA200 : %.5f Close: %.5f",
g_EMA200[1], g_Bars[1].close)); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "atr", xLeft, y, CORNER_LEFT_UPPER,
clrSilver,
StringFormat("ATR : %.5f", g_ATR[1])); y += rowH;
string contextLine = "";
if (g_State == STATE_PENDING_ORDER)
contextLine = StringFormat("Pending: dir=%s entry=%.5f alive=%d/%d",
(g_Pending.plan.direction > 0 ? "BUY" : "SELL"),
g_Pending.plan.entryPrice,
g_Pending.barsSincePlaced,
InpPendingMaxAliveBars);
else if (g_State == STATE_IN_TRADE)
contextLine = StringFormat("Trade : dir=%s entry=%.5f partial=%s",
(g_OpenTrade.plan.direction > 0 ? "BUY" : "SELL"),
g_OpenTrade.plan.entryPrice,
g_OpenTrade.partialClosedDone ? "DONE" : "PEND");
else if (g_State == STATE_WATCHING)
contextLine = StringFormat("Watch : %d/%d bars",
g_BarsInWatching, InpWatchingMaxBars);
else
contextLine = "Idle : looking for pullback...";
EnsureLabel(VIS_LBL_DASH_PREFIX "context", xLeft, y, CORNER_LEFT_UPPER,
clrOrange, contextLine);
}
// ------------------------------------------------------------
// Bottom-left stats panel
// ------------------------------------------------------------
bool IsDealOurs(const ulong dealTicket)
{
if (HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != InpMagicNumber) return false;
if (HistoryDealGetString (dealTicket, DEAL_SYMBOL) != _Symbol) return false;
return true;
}
bool ContainsULong(const ulong &arr[], const int count, const ulong needle)
{
for (int i = 0; i < count; i++)
if (arr[i] == needle) return true;
return false;
}
void ComputeTradeStats(VisualTradeStats &stats)
{
ZeroMemory(stats);
const datetime fromTime = TimeCurrent() - (datetime)(InpStatsLookbackDays * 86400);
if (!HistorySelect(fromTime, TimeCurrent())) return;
ulong seenPositions[];
int seenCount = 0;
const int dealsTotal = HistoryDealsTotal();
for (int i = 0; i < dealsTotal; i++)
{
const ulong dealTicket = HistoryDealGetTicket(i);
if (dealTicket == 0) continue;
if (!IsDealOurs(dealTicket)) continue;
if (HistoryDealGetInteger(dealTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
const long reason = HistoryDealGetInteger(dealTicket, DEAL_REASON);
const ulong positionId = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
const double profit = HistoryDealGetDouble (dealTicket, DEAL_PROFIT)
+ HistoryDealGetDouble (dealTicket, DEAL_SWAP)
+ HistoryDealGetDouble (dealTicket, DEAL_COMMISSION);
stats.netPL += profit;
if (reason == DEAL_REASON_TP) stats.tpHits++;
else if (reason == DEAL_REASON_SL) stats.slHits++;
else stats.otherCloses++;
if (positionId > 0 && !ContainsULong(seenPositions, seenCount, positionId))
{
ArrayResize(seenPositions, seenCount + 1);
seenPositions[seenCount++] = positionId;
// Count as "fully closed" only if the position no longer exists.
if (!PositionSelectByTicket(positionId)) stats.totalClosed++;
}
}
}
void DrawStatsPanel()
{
if (!InpShowStatsPanel) return;
VisualTradeStats stats;
ComputeTradeStats(stats);
const int xLeft = 12;
const int rowH = 18;
int y = 12;
EnsureLabel(VIS_LBL_STATS_PREFIX "pl", xLeft, y, CORNER_LEFT_LOWER,
stats.netPL >= 0 ? clrLime : clrTomato,
StringFormat("Net PL : %.2f", stats.netPL)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "other", xLeft, y, CORNER_LEFT_LOWER,
clrSilver,
StringFormat("Other : %d", stats.otherCloses)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "sl", xLeft, y, CORNER_LEFT_LOWER,
InpColorSL,
StringFormat("SL hit : %d", stats.slHits)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "tp", xLeft, y, CORNER_LEFT_LOWER,
InpColorTP,
StringFormat("TP hit : %d", stats.tpHits)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "total", xLeft, y, CORNER_LEFT_LOWER,
clrWhite,
StringFormat("Total : %d", stats.totalClosed)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "title", xLeft, y, CORNER_LEFT_LOWER,
clrWhite,
StringFormat("-- STATS (last %dd) --", InpStatsLookbackDays), 10);
}
// ------------------------------------------------------------
// Entry / SL / TP horizontal lines (TradingView style)
// ------------------------------------------------------------
void RemoveTradeLevels()
{
ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "entry");
ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "sl");
ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "tp");
}
void DrawTradeLevels()
{
if (!InpShowTradeLevels) { RemoveTradeLevels(); return; }
SignalSnapshot plan;
ZeroMemory(plan);
bool show = false;
if (g_State == STATE_PENDING_ORDER)
{
plan = g_Pending.plan;
show = true;
}
else if (g_State == STATE_IN_TRADE)
{
plan = g_OpenTrade.plan;
show = true;
}
if (!show) { RemoveTradeLevels(); return; }
const string sideStr = (plan.direction > 0) ? "BUY" : "SELL";
EnsureHLine(VIS_OBJ_LEVEL_PREFIX "entry", plan.entryPrice,
InpColorEntry, STYLE_DOT, 1,
StringFormat("%s ENTRY %.5f", sideStr, plan.entryPrice));
EnsureHLine(VIS_OBJ_LEVEL_PREFIX "sl", plan.stopLossPrice,
InpColorSL, STYLE_DASH, 1,
StringFormat("SL %.5f", plan.stopLossPrice));
EnsureHLine(VIS_OBJ_LEVEL_PREFIX "tp", plan.takeProfitPrice,
InpColorTP, STYLE_DASH, 1,
StringFormat("TP %.5f", plan.takeProfitPrice));
}
// ------------------------------------------------------------
// Indicator attach (EMA200 main + RSI cluster in subwindow)
// ------------------------------------------------------------
// Returns true if any indicator named like `prefix*` already lives on the
// given subwindow. Used to avoid stacking duplicates when the EA is reloaded.
bool IsIndicatorAlreadyAttached(const int subWindow, const string namePrefix)
{
const int total = ChartIndicatorsTotal(0, subWindow);
for (int i = 0; i < total; i++)
{
const string n = ChartIndicatorName(0, subWindow, i);
if (StringFind(n, namePrefix) == 0) return true;
}
return false;
}
void AttachIndicatorsToChart()
{
if (!InpAttachIndicators) return;
// EMA200 in main window.
if (!IsIndicatorAlreadyAttached(0, "Moving Average"))
{
if (!ChartIndicatorAdd(0, 0, g_hEMA200))
PrintFormat("[VIS] add EMA200 to main fail: %d", GetLastError());
}
// RSI cluster in a dedicated subwindow.
// We try to find an existing subwindow that already hosts an RSI; if not,
// pass CHART_WINDOWS_TOTAL to create a fresh one.
int rsiSubWindow = -1;
const int totalWindows = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
for (int w = 1; w < totalWindows; w++)
{
if (IsIndicatorAlreadyAttached(w, "RSI"))
{
rsiSubWindow = w;
break;
}
}
if (rsiSubWindow < 0) rsiSubWindow = totalWindows; // create new
if (!IsIndicatorAlreadyAttached(rsiSubWindow, "RSI"))
{
if (!ChartIndicatorAdd(0, rsiSubWindow, g_hRSI))
PrintFormat("[VIS] add RSI to subwindow fail: %d", GetLastError());
}
if (!IsIndicatorAlreadyAttached(rsiSubWindow, "Moving Average"))
{
if (!ChartIndicatorAdd(0, rsiSubWindow, g_hEMA9))
PrintFormat("[VIS] add RSI_EMA9 to subwindow fail: %d", GetLastError());
if (!ChartIndicatorAdd(0, rsiSubWindow, g_hWMA45))
PrintFormat("[VIS] add RSI_WMA45 to subwindow fail: %d", GetLastError());
}
ChartRedraw(0);
}
// ------------------------------------------------------------
// Top-level orchestration
// ------------------------------------------------------------
void DrawAllVisuals(const TrendDirection trendNow)
{
if (!InpVisualize) return;
DrawDashboardPanel(trendNow);
DrawStatsPanel();
DrawTradeLevels();
}
void RemoveAllVisuals()
{
ObjectsDeleteAll(0, VIS_PREFIX);
ChartRedraw(0);
}
#endif