367 lines
13 KiB
Plaintext
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
|