MQL5-SyntheticTestHarness/MQL5/Include/SyntheticTestHarness/CCanvasVisualizer.mqh

348 lines
13 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| CCanvasVisualizer.mqh |
//| Synthetic Test Harness for MetaTrader 5 |
//| Institutional Architecture in MQL5 - Part 1 Series |
//+------------------------------------------------------------------+
#ifndef SYNTHETIC_TEST_HARNESS_CCANVAS_VISUALIZER_MQH
#define SYNTHETIC_TEST_HARNESS_CCANVAS_VISUALIZER_MQH
#include <Canvas\Canvas.mqh>
#include "MockTick.mqh"
#include "ErrorCodes.mqh"
//+------------------------------------------------------------------+
//| OHLC: represents a consolidated candle structure for rendering. |
//+------------------------------------------------------------------+
struct OHLC
{
double open;
double high;
double low;
double close;
bool is_bullish;
bool has_error;
};
//+------------------------------------------------------------------+
//| LayoutMetrics: holds pixel dimensions and bounds for rendering. |
//+------------------------------------------------------------------+
struct LayoutMetrics
{
int canvas_w; // Total canvas width (pixels)
int canvas_h; // Total canvas height (pixels)
int draw_x; // Left edge of the drawing area
int draw_y; // Top edge of the drawing area
int draw_w; // Drawing area width
int draw_h; // Drawing area height
int bar_width; // Width of each candle body (pixels)
int gap_width; // Gap between candles (pixels)
double price_min; // Lowest price in the scenario
double price_max; // Highest price in the scenario
double price_range; // price_max - price_min (with padding)
};
//+------------------------------------------------------------------+
//| CCanvasVisualizer: visual overlay for synthetic scenarios. |
//+------------------------------------------------------------------+
class CCanvasVisualizer
{
private:
CCanvas m_canvas;
bool m_is_created;
2026-07-23 15:19:34 -03:00
bool m_original_show;
//+---------------------------------------------------------------+
//| CreateOverlay: initializes the canvas overlay over the chart. |
//+---------------------------------------------------------------+
bool CreateOverlay(void)
{
int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
if(!m_canvas.CreateBitmapLabel(0, 0, "STH_Overlay", 0, 0, width, height, COLOR_FORMAT_XRGB_NOALPHA))
{
Print("CCanvasVisualizer: Failed to create Canvas.");
m_is_created = false;
return(false);
}
m_is_created = true;
m_canvas.Erase(ColorToARGB(clrWhite, 255));
2026-07-23 15:19:34 -03:00
// Save original show state and temporarily hide the user's candles/grids
m_original_show = (bool)ChartGetInteger(0, CHART_SHOW);
ChartSetInteger(0, CHART_SHOW, false);
return(true);
}
//+---------------------------------------------------------------+
//| BuildOHLC: converts raw MockTicks to consolidated OHLC bars. |
//+---------------------------------------------------------------+
void BuildOHLC(const MockTick &ticks[], OHLC &bars[])
{
int count = ArraySize(ticks);
ArrayResize(bars, count);
if(count == 0)
return;
for(int i = 0; i < count; i++)
{
// Open is the bid of the previous tick. For index 0, use own bid.
bars[i].open = (i == 0) ? ticks[0].bid : ticks[i - 1].bid;
bars[i].close = ticks[i].bid;
// High and low encompass the full bid/ask spread and open/close boundaries.
double high_val = MathMax(ticks[i].bid, ticks[i].ask);
double low_val = MathMin(ticks[i].bid, ticks[i].ask);
bars[i].high = MathMax(high_val, MathMax(bars[i].open, bars[i].close));
bars[i].low = MathMin(low_val, MathMin(bars[i].open, bars[i].close));
bars[i].is_bullish = (bars[i].close >= bars[i].open);
bars[i].has_error = IsExecutionError(ticks[i].injected_error);
}
}
//+---------------------------------------------------------------+
//| ComputeLayout: calculates positioning metrics for elements. |
//+---------------------------------------------------------------+
void ComputeLayout(const OHLC &bars[], LayoutMetrics &m)
{
int count = ArraySize(bars);
m.canvas_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
m.canvas_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
// Define centered drawing area (70% width, 70% height)
m.draw_w = (int)(m.canvas_w * 0.70);
m.draw_x = (int)(m.canvas_w * 0.15);
m.draw_h = (int)(m.canvas_h * 0.70);
m.draw_y = (int)(m.canvas_h * 0.15);
// Find price range min/max
m.price_min = DBL_MAX;
m.price_max = -DBL_MAX;
for(int i = 0; i < count; i++)
{
if(bars[i].low < m.price_min)
m.price_min = bars[i].low;
if(bars[i].high > m.price_max)
m.price_max = bars[i].high;
}
// Add 10% vertical buffer to prevent crowding near boundaries
double diff = m.price_max - m.price_min;
if(diff <= 0.0)
diff = 0.0001;
m.price_min -= diff * 0.10;
m.price_max += diff * 0.10;
m.price_range = m.price_max - m.price_min;
// Candle body width and gap sizing
if(count > 0)
{
m.bar_width = m.draw_w / (2 * count - 1);
if(m.bar_width < 1)
m.bar_width = 1;
m.gap_width = m.bar_width;
}
else
{
m.bar_width = 10;
m.gap_width = 10;
}
}
//+---------------------------------------------------------------+
//| PriceToY: scales double prices to chart pixel coordinates. |
//+---------------------------------------------------------------+
int PriceToY(const double price, const LayoutMetrics &m)
{
if(m.price_range <= 0.0)
return(m.draw_y + m.draw_h / 2);
return(m.draw_y + m.draw_h - (int)(((price - m.price_min) / m.price_range) * m.draw_h));
}
//+---------------------------------------------------------------+
//| DrawCandles: renders the wicks and bodies of the mock candles.|
//+---------------------------------------------------------------+
void DrawCandles(const OHLC &bars[], const LayoutMetrics &m)
{
int count = ArraySize(bars);
uint bull_color = ColorToARGB(clrForestGreen, 255);
uint bear_color = ColorToARGB(clrFireBrick, 255);
uint error_color = ColorToARGB(clrOrange, 255);
for(int i = 0; i < count; i++)
{
int bar_x_center = m.draw_x + i * (m.bar_width + m.gap_width) + m.bar_width / 2;
int bar_x1 = bar_x_center - m.bar_width / 2;
int bar_x2 = bar_x1 + m.bar_width - 1;
int y_open = PriceToY(bars[i].open, m);
int y_close = PriceToY(bars[i].close, m);
int y_high = PriceToY(bars[i].high, m);
int y_low = PriceToY(bars[i].low, m);
// Color assignment - Option A: highlight whole candle in orange if error injected
2026-07-23 15:19:34 -03:00
uint candle_color = bars[i].is_bullish ? bull_color : bear_color;
if(bars[i].has_error)
2026-07-23 15:19:34 -03:00
candle_color = error_color;
// Draw shadow line (wick)
2026-07-23 15:19:34 -03:00
m_canvas.LineVertical(bar_x_center, y_high, y_low, candle_color);
// Draw candle body
int body_y1 = MathMin(y_open, y_close);
int body_y2 = MathMax(y_open, y_close);
if(body_y1 == body_y2)
body_y2 = body_y1 + 1; // Guarantee visibility
2026-07-23 15:19:34 -03:00
m_canvas.FillRectangle(bar_x1, body_y1, bar_x2, body_y2, candle_color);
}
}
//+---------------------------------------------------------------+
//| DrawPriceAxis: draws gridlines and right-side price values. |
//+---------------------------------------------------------------+
void DrawPriceAxis(const LayoutMetrics &m)
{
int axis_x = m.draw_x + m.draw_w;
uint axis_color = ColorToARGB(clrLightGray, 255);
uint text_color = ColorToARGB(clrDimGray, 255);
uint grid_color = ColorToARGB(clrGray, 40); // Soft transparent horizontal grid
// Vertical axis line
m_canvas.LineVertical(axis_x, m.draw_y, m.draw_y + m.draw_h, axis_color);
m_canvas.FontSet("Arial", 11, FW_NORMAL);
// Render 5 price grid ticks (0%, 25%, 50%, 75%, 100%)
for(int i = 0; i <= 4; i++)
{
double ratio = i / 4.0;
double price = m.price_min + ratio * m.price_range;
int y = PriceToY(price, m);
// Grid line
m_canvas.LineHorizontal(m.draw_x, axis_x, y, grid_color);
// Label text
string label = DoubleToString(price, 5);
m_canvas.TextOut(axis_x + 8, y - 6, label, text_color);
}
}
//+---------------------------------------------------------------+
//| DrawTitle: writes main title and scenario summary text. |
//+---------------------------------------------------------------+
void DrawTitle(const MockTick &ticks[], const LayoutMetrics &m)
{
int count = ArraySize(ticks);
if(count == 0)
return;
uint title_color = ColorToARGB(clrBlack, 255);
uint sub_color = ColorToARGB(clrDimGray, 255);
// Main title
m_canvas.FontSet("Arial", 20, FW_BOLD);
string title = "Synthetic Test Harness - Scenario Preview";
int title_x = (m.canvas_w - m_canvas.TextWidth(title)) / 2;
m_canvas.TextOut(title_x, m.draw_y / 3, title, title_color);
// Subtitle
m_canvas.FontSet("Arial", 12, FW_NORMAL);
datetime start_time = ticks[0].time;
datetime end_time = ticks[count - 1].time;
string subtitle = StringFormat("%d bars | %s -> %s", count, TimeToString(start_time), TimeToString(end_time));
int sub_x = (m.canvas_w - m_canvas.TextWidth(subtitle)) / 2;
m_canvas.TextOut(sub_x, m.draw_y / 3 + 28, subtitle, sub_color);
}
//+---------------------------------------------------------------+
//| DrawCountdown: updates bottom-left countdown label. |
//+---------------------------------------------------------------+
void DrawCountdown(const int seconds_remaining, const LayoutMetrics &m)
{
// Erase previous text space
m_canvas.FillRectangle(0, m.canvas_h - 60, 450, m.canvas_h, ColorToARGB(clrWhite, 255));
m_canvas.FontSet("Arial", 14, FW_BOLD);
string text = StringFormat("Restoring chart in: %ds", seconds_remaining);
m_canvas.TextOut(20, m.canvas_h - 40, text, ColorToARGB(clrDimGray, 255));
m_canvas.Update();
}
//+---------------------------------------------------------------+
//| DestroyOverlay: removes the canvas and triggers chart redraw. |
//+---------------------------------------------------------------+
void DestroyOverlay(void)
{
if(m_is_created)
{
m_canvas.Destroy();
m_is_created = false;
2026-07-23 15:19:34 -03:00
// Restore original chart show state
ChartSetInteger(0, CHART_SHOW, m_original_show);
ChartRedraw(0);
}
}
public:
//+---------------------------------------------------------------+
//| Constructor. |
//+---------------------------------------------------------------+
CCanvasVisualizer(void)
{
m_is_created = false;
}
//+---------------------------------------------------------------+
//| Destructor: ensures overlay is cleaned up on stack release. |
//+---------------------------------------------------------------+
~CCanvasVisualizer(void)
{
DestroyOverlay();
}
//+---------------------------------------------------------------+
//| ShowScenario: runs the visualizer pipeline and countdown. |
//+---------------------------------------------------------------+
void ShowScenario(const MockTick &ticks[], int countdown_seconds = 5)
{
if(ArraySize(ticks) == 0)
return;
if(!CreateOverlay())
return;
OHLC bars[];
LayoutMetrics m;
BuildOHLC(ticks, bars);
ComputeLayout(bars, m);
DrawPriceAxis(m);
DrawCandles(bars, m);
DrawTitle(ticks, m);
// Countdown loop
for(int i = countdown_seconds; i > 0; i--)
{
DrawCountdown(i, m);
Sleep(1000);
}
DestroyOverlay();
}
};
//+------------------------------------------------------------------+
#endif