mql5/Indicators/RENKO/RenkoMoscow.mq5
2026-11-05 12:52:15 +03:00
854 lines
No EOL
29 KiB
MQL5
#property strict
#property indicator_separate_window
#property indicator_plots 1
#property indicator_buffers 5
#property indicator_type1 DRAW_NONE
#property indicator_color1 clrLimeGreen, clrTomato
#property indicator_width1 1
#property indicator_label1 "Renko bricks"
#property version "1.00"
#property description "Configurable Renko brick size in points; cycle starts at 00:03 Moscow"
input int InpBrickPoints = 10; // Renko brick size, points
input int InpHistoryDays = 150; // Initial history window, days
input int InpMinimumHistoryBricks = 500; // Extend history until at least this many bricks are built
input int InpMaximumHistoryDays = 3650; // Maximum history search window, days
input color InpUpColor = clrLimeGreen;
input color InpDownColor = clrTomato;
input int InpRightEdgeGapPixels = 40; // Empty space after the newest Renko brick, pixels
struct RenkoBrick
{
datetime time;
double open;
double high;
double low;
double close;
int direction;
};
double g_brick_size = 0.0;
double g_last_close = 0.0;
double g_last_bid = 0.0;
datetime g_cycle_start = 0;
int g_direction = 0; // 1 = up, -1 = down, 0 = not established
long g_last_tick_msc = 0;
bool g_have_anchor = false;
bool g_history_initialized = false;
bool g_alert_enabled = true;
string g_alert_button_name = "";
double g_open_buffer[];
double g_high_buffer[];
double g_low_buffer[];
double g_close_buffer[];
double g_color_buffer[];
RenkoBrick g_bricks[];
int g_brick_count = 0;
string g_brick_object_prefix = "";
int g_rendered_object_count = 0;
int g_last_rendered_brick_count = -1;
int g_last_chart_width = -1;
int g_last_subwindow_height = -1;
// Independent Renko viewport: one-past-last visible brick and pixels per brick.
int g_view_end = -1;
int g_cell_width = 12;
int g_drag_last_x = -1;
double g_drag_remainder_x = 0.0;
double g_linked_price_min = 0.0;
double g_linked_price_max = 0.0;
bool g_linked_price_scale_ready = false;
bool g_mouse_move_previous = false;
bool g_mouse_wheel_previous = false;
// Returns the 00:03 boundary of the daily cycle containing t.
// Tick timestamps are treated as Moscow time; no server-to-Moscow conversion is applied.
datetime CycleStart(datetime t)
{
MqlDateTime dt;
if(!TimeToStruct(t, dt))
return 0;
if(dt.hour == 0 && dt.min < 3)
t -= 86400;
if(!TimeToStruct(t, dt))
return 0;
dt.hour = 0;
dt.min = 3;
dt.sec = 0;
return StructToTime(dt);
}
void ResetCycle(datetime cycle_start)
{
g_cycle_start = cycle_start;
g_last_close = 0.0;
g_direction = 0;
g_have_anchor = false;
}
void DrawBrick(datetime tick_time, double open_price, double close_price, int direction)
{
const int new_count = g_brick_count + 1;
if(ArrayResize(g_bricks, new_count, 256) != new_count)
{
Print("Renko5PointsMoscow: failed to allocate brick storage, error=", GetLastError());
return;
}
g_bricks[g_brick_count].time = tick_time;
g_bricks[g_brick_count].open = open_price;
g_bricks[g_brick_count].high = MathMax(open_price, close_price);
g_bricks[g_brick_count].low = MathMin(open_price, close_price);
g_bricks[g_brick_count].close = close_price;
g_bricks[g_brick_count].direction = direction;
g_brick_count = new_count;
}
void RenderBricks(const int rates_total)
{
for(int i = 0; i < rates_total; ++i)
{
g_open_buffer[i] = 0.0;
g_high_buffer[i] = 0.0;
g_low_buffer[i] = 0.0;
g_close_buffer[i] = 0.0;
g_color_buffer[i] = 0.0;
}
int count = g_brick_count;
if(count > rates_total)
count = rates_total;
// Renko has its own brick sequence, not a time-based candle sequence.
// Put the newest brick at buffer index 0 and older bricks to its left.
for(int i = 0; i < count; ++i)
{
const int source = g_brick_count - 1 - i;
g_open_buffer[i] = g_bricks[source].open;
g_high_buffer[i] = g_bricks[source].high;
g_low_buffer[i] = g_bricks[source].low;
g_close_buffer[i] = g_bricks[source].close;
g_color_buffer[i] = (g_bricks[source].direction > 0 ? 0.0 : 1.0);
}
}
// Keep a visible, configurable gap between the newest brick and the right chart edge.
int GetRightEdgeGapPixels(const int chart_width)
{
const int minimum_left_margin = 8;
const int minimum_brick_width = 5;
const int requested_gap = (int)MathMax(8, InpRightEdgeGapPixels);
const int maximum_gap = (int)MathMax(0, chart_width - minimum_left_margin - minimum_brick_width);
return (int)MathMin(requested_gap, maximum_gap);
}
int GetVisibleBrickCapacity(const int chart_width, const int cell_width)
{
const int left_margin = 8;
const int right_gap = GetRightEdgeGapPixels(chart_width);
const int available_width = chart_width - left_margin - right_gap;
return (int)MathMax(1, available_width / MathMax(1, cell_width));
}
// Synchronize the Renko subwindow's numeric price range with the main chart.
// Horizontal Renko spacing remains independent; only the vertical price scale is linked.
bool UpdateLinkedPriceScale(double &scale_min,
double &scale_max,
bool &scale_changed)
{
scale_changed = false;
double main_min = 0.0;
double main_max = 0.0;
if(!ChartGetDouble(0, CHART_PRICE_MIN, 0, main_min) ||
!ChartGetDouble(0, CHART_PRICE_MAX, 0, main_max) ||
!MathIsValidNumber(main_min) ||
!MathIsValidNumber(main_max) ||
main_max <= main_min)
{
if(!g_linked_price_scale_ready)
return false;
scale_min = g_linked_price_min;
scale_max = g_linked_price_max;
return true;
}
const double tolerance = MathMax(_Point * 0.1, 1.0e-12);
const bool changed = (!g_linked_price_scale_ready ||
MathAbs(main_min - g_linked_price_min) > tolerance ||
MathAbs(main_max - g_linked_price_max) > tolerance);
if(changed)
{
bool minimum_set = false;
bool maximum_set = false;
const bool new_range_below = (g_linked_price_scale_ready
? main_max < g_linked_price_min
: main_max < 0.0);
if(new_range_below)
{
minimum_set = IndicatorSetDouble(INDICATOR_MINIMUM, main_min);
maximum_set = IndicatorSetDouble(INDICATOR_MAXIMUM, main_max);
}
else
{
maximum_set = IndicatorSetDouble(INDICATOR_MAXIMUM, main_max);
minimum_set = IndicatorSetDouble(INDICATOR_MINIMUM, main_min);
}
if(!minimum_set || !maximum_set)
{
if(g_linked_price_scale_ready)
{
IndicatorSetDouble(INDICATOR_MINIMUM, g_linked_price_min);
IndicatorSetDouble(INDICATOR_MAXIMUM, g_linked_price_max);
scale_min = g_linked_price_min;
scale_max = g_linked_price_max;
return true;
}
return false;
}
g_linked_price_min = main_min;
g_linked_price_max = main_max;
g_linked_price_scale_ready = true;
scale_changed = true;
}
scale_min = g_linked_price_min;
scale_max = g_linked_price_max;
return true;
}
// Draw bricks by their true price levels inside the independent Renko viewport.
// The vertical mapping uses the main chart's visible price range.
void RenderPixelBricks()
{
const string short_name = StringFormat("Renko %d points / 00:03 Moscow", InpBrickPoints);
const int subwindow = ChartWindowFind(0, short_name);
if(subwindow < 1 || g_brick_count <= 0)
return;
long width_value = 0;
long height_value = 0;
long y_offset_value = 0;
if(!ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0, width_value) ||
!ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, subwindow, height_value) ||
!ChartGetInteger(0, CHART_WINDOW_YDISTANCE, subwindow, y_offset_value))
return;
const int chart_width = (int)width_value;
const int window_height = (int)height_value;
const int window_top = (int)y_offset_value;
if(chart_width <= 24 || window_height <= 24)
return;
double scale_min = 0.0;
double scale_max = 0.0;
bool scale_changed = false;
if(!UpdateLinkedPriceScale(scale_min, scale_max, scale_changed))
return;
// Make the terminal apply a changed indicator scale before converting prices.
if(scale_changed)
ChartRedraw(0);
if(g_brick_object_prefix == "")
g_brick_object_prefix = StringFormat("RM%d_%I64d_", InpBrickPoints, ChartID());
const int margin_x = 8;
const int right_edge_gap = GetRightEdgeGapPixels(chart_width);
const int available_width = chart_width - margin_x - right_edge_gap;
int visible_count = available_width / g_cell_width;
if(visible_count < 1)
visible_count = 1;
if(visible_count > g_brick_count)
visible_count = g_brick_count;
if(g_view_end < 0 || g_view_end > g_brick_count)
g_view_end = g_brick_count;
if(g_view_end < visible_count)
g_view_end = visible_count;
const int first_brick = g_view_end - visible_count;
const int x_start = chart_width - right_edge_gap - visible_count * g_cell_width;
datetime coordinate_time = iTime(_Symbol, _Period, 0);
if(coordinate_time <= 0)
coordinate_time = g_bricks[first_brick].time;
for(int i = 0; i < visible_count; ++i)
{
const int source = first_brick + i;
const string object_name = g_brick_object_prefix + IntegerToString(i);
const int x = x_start + i * g_cell_width;
int high_x = 0;
int high_y = 0;
int low_x = 0;
int low_y = 0;
const bool high_ok = ChartTimePriceToXY(0, subwindow, coordinate_time,
g_bricks[source].high, high_x, high_y);
const bool low_ok = ChartTimePriceToXY(0, subwindow, coordinate_time,
g_bricks[source].low, low_x, low_y);
if(!high_ok || !low_ok)
{
ObjectDelete(0, object_name);
continue;
}
// ChartTimePriceToXY returns chart-global Y; graphical objects in a
// subwindow use coordinates relative to that subwindow's upper edge.
const int high_local_y = high_y - window_top;
const int low_local_y = low_y - window_top;
const int raw_top = (high_local_y < low_local_y ? high_local_y : low_local_y);
const int raw_bottom = (high_local_y > low_local_y ? high_local_y : low_local_y);
if(raw_bottom <= 0 || raw_top >= window_height)
{
ObjectDelete(0, object_name);
continue;
}
const int y_top = (int)MathMax(0, MathMin(window_height, raw_top));
int y_bottom = (int)MathMax(0, MathMin(window_height, raw_bottom));
if(y_bottom <= y_top)
y_bottom = (int)MathMin(window_height, y_top + 1);
if(y_bottom <= y_top)
{
ObjectDelete(0, object_name);
continue;
}
const color brick_color = (g_bricks[source].direction > 0 ? InpUpColor : InpDownColor);
if(ObjectFind(0, object_name) < 0)
{
if(!ObjectCreate(0, object_name, OBJ_RECTANGLE_LABEL, subwindow, 0, 0))
{
Print("Renko5PointsMoscow: failed to create brick object, error=", GetLastError());
continue;
}
}
ObjectSetInteger(0, object_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, object_name, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, object_name, OBJPROP_YDISTANCE, y_top);
ObjectSetInteger(0, object_name, OBJPROP_XSIZE, g_cell_width);
ObjectSetInteger(0, object_name, OBJPROP_YSIZE, y_bottom - y_top);
ObjectSetInteger(0, object_name, OBJPROP_BGCOLOR, brick_color);
ObjectSetInteger(0, object_name, OBJPROP_COLOR, brick_color);
ObjectSetInteger(0, object_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, object_name, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, object_name, OBJPROP_BACK, false);
ObjectSetInteger(0, object_name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, object_name, OBJPROP_SELECTED, false);
ObjectSetInteger(0, object_name, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, object_name, OBJPROP_ZORDER, 0);
}
for(int i = visible_count; i < g_rendered_object_count; ++i)
ObjectDelete(0, g_brick_object_prefix + IntegerToString(i));
g_rendered_object_count = visible_count;
g_last_rendered_brick_count = g_brick_count;
g_last_chart_width = chart_width;
g_last_subwindow_height = window_height;
ChartRedraw(0);
}
void ProcessBid(datetime tick_time, double bid, const bool enable_alert)
{
if(bid <= 0.0 || g_brick_size <= 0.0)
return;
const datetime tick_cycle = CycleStart(tick_time);
if(tick_cycle == 0)
return;
if(tick_cycle != g_cycle_start)
ResetCycle(tick_cycle);
// Before 00:03, CycleStart() assigns ticks to the previous day's cycle.
if(!g_have_anchor)
{
g_last_close = NormalizeDouble(bid, _Digits);
g_last_bid = bid;
g_have_anchor = true;
return;
}
int safety = 0;
bool changed = true;
while(changed && safety++ < 10000)
{
changed = false;
if(g_direction == 0)
{
if(bid >= g_last_close + g_brick_size)
{
const double next_close = g_last_close + g_brick_size;
DrawBrick(tick_time, g_last_close, next_close, 1);
g_last_close = next_close;
g_direction = 1;
changed = true;
}
else if(bid <= g_last_close - g_brick_size)
{
const double next_close = g_last_close - g_brick_size;
DrawBrick(tick_time, g_last_close, next_close, -1);
g_last_close = next_close;
g_direction = -1;
changed = true;
}
}
else if(g_direction > 0)
{
if(bid >= g_last_close + g_brick_size)
{
const double next_close = g_last_close + g_brick_size;
DrawBrick(tick_time, g_last_close, next_close, 1);
g_last_close = next_close;
changed = true;
}
else if(bid <= g_last_close - 2.0 * g_brick_size)
{
const double brick_open = g_last_close - g_brick_size;
const double next_close = g_last_close - 2.0 * g_brick_size;
DrawBrick(tick_time, brick_open, next_close, -1);
g_last_close = next_close;
g_direction = -1;
if(enable_alert && g_alert_enabled)
Alert(InpBrickPoints, " bricks DOWN ", _Symbol);
changed = true;
}
}
else
{
if(bid <= g_last_close - g_brick_size)
{
const double next_close = g_last_close - g_brick_size;
DrawBrick(tick_time, g_last_close, next_close, -1);
g_last_close = next_close;
changed = true;
}
else if(bid >= g_last_close + 2.0 * g_brick_size)
{
const double brick_open = g_last_close + g_brick_size;
const double next_close = g_last_close + 2.0 * g_brick_size;
DrawBrick(tick_time, brick_open, next_close, 1);
g_last_close = next_close;
g_direction = 1;
if(enable_alert && g_alert_enabled)
Alert(InpBrickPoints, " bricks UP ", _Symbol);
changed = true;
}
}
}
g_last_bid = bid;
}
bool RebuildHistoryForDays(const int requested_days)
{
ArrayResize(g_bricks, 0);
g_brick_count = 0;
const datetime now = TimeCurrent();
const datetime current_cycle = CycleStart(now);
if(current_cycle == 0)
return false;
ResetCycle(0);
g_last_tick_msc = 0;
g_last_bid = 0.0;
int days = requested_days;
if(days < 1)
days = 1;
if(days > 3650)
days = 3650;
int total_ticks_copied = 0;
for(int day = days - 1; day >= 0; --day)
{
const datetime from_time = current_cycle - (datetime)(day * 86400);
const datetime to_time = from_time + 86400;
MqlTick ticks[];
ResetLastError();
const int copied = CopyTicksRange(_Symbol, ticks, COPY_TICKS_INFO,
(ulong)from_time * 1000,
(ulong)to_time * 1000 - 1);
if(copied < 0)
{
const int error = GetLastError();
Print("Renko5PointsMoscow: CopyTicksRange failed, error=", error,
", cycle=", TimeToString(from_time, TIME_DATE|TIME_MINUTES),
(error == 4401
? ". No tick history for this cycle; skipping this range."
: ". Skipping this range; live tick processing remains active."));
continue;
}
total_ticks_copied += copied;
for(int i = 0; i < copied; ++i)
{
if(ticks[i].bid <= 0.0)
continue;
ProcessBid((datetime)ticks[i].time, ticks[i].bid, false);
if(ticks[i].time_msc > g_last_tick_msc)
g_last_tick_msc = ticks[i].time_msc;
g_last_bid = ticks[i].bid;
}
}
if(total_ticks_copied == 0)
Print("Renko5PointsMoscow: no ticks returned for the requested history window; starting with live ticks.");
Print("Renko5PointsMoscow: history window=", days,
" days; ticks=", total_ticks_copied,
"; built bricks=", g_brick_count);
return true;
}
bool RebuildHistory()
{
int days = InpHistoryDays;
if(days < 1)
days = 1;
int maximum_days = InpMaximumHistoryDays;
if(maximum_days < 1)
maximum_days = 1;
if(maximum_days > 3650)
maximum_days = 3650;
if(days > maximum_days)
days = maximum_days;
int minimum_bricks = InpMinimumHistoryBricks;
if(minimum_bricks < 1)
minimum_bricks = 1;
while(true)
{
if(!RebuildHistoryForDays(days))
return false;
if(g_brick_count >= minimum_bricks || days >= maximum_days)
break;
const int previous_days = days;
if(days > maximum_days / 2)
days = maximum_days;
else
days *= 2;
Print("Renko5PointsMoscow: only ", g_brick_count,
" bricks in ", previous_days,
" days; expanding requested history to ", days, " days.");
}
if(g_brick_count < minimum_bricks)
Print("Renko5PointsMoscow: requested minimum ", minimum_bricks,
" bricks not reached. Available history produced ", g_brick_count,
" bricks after checking ", days,
" days. The terminal may not have sufficient tick history.");
return true;
}
bool CreateAlertButton()
{
if(g_alert_button_name != "")
return true;
const string short_name = StringFormat("Renko %d points / 00:03 Moscow", InpBrickPoints);
const int subwindow = ChartWindowFind(0, short_name);
if(subwindow < 0)
return false;
g_alert_button_name = StringFormat("Renko%d.AlertToggle.%d", InpBrickPoints, subwindow);
if(ObjectFind(0, g_alert_button_name) >= 0)
return true;
if(!ObjectCreate(0, g_alert_button_name, OBJ_BUTTON, subwindow, 0, 0))
{
g_alert_button_name = "";
return false;
}
ObjectSetInteger(0, g_alert_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, g_alert_button_name, OBJPROP_XDISTANCE, 8);
ObjectSetInteger(0, g_alert_button_name, OBJPROP_YDISTANCE, 35);
ObjectSetInteger(0, g_alert_button_name, OBJPROP_XSIZE, 90);
ObjectSetInteger(0, g_alert_button_name, OBJPROP_YSIZE, 22);
ObjectSetInteger(0, g_alert_button_name, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, g_alert_button_name, OBJPROP_TEXT,
(g_alert_enabled ? "Alert: ON" : "Alert: OFF"));
ObjectSetInteger(0, g_alert_button_name, OBJPROP_STATE, false);
return true;
}
int OnInit()
{
if(InpBrickPoints < 1 || InpMinimumHistoryBricks < 1 || InpMaximumHistoryDays < 1)
{
Print("Renko5PointsMoscow: brick size, minimum history bricks, and maximum history days must be >= 1.");
return INIT_PARAMETERS_INCORRECT;
}
if(!SetIndexBuffer(0, g_open_buffer, INDICATOR_DATA) ||
!SetIndexBuffer(1, g_high_buffer, INDICATOR_DATA) ||
!SetIndexBuffer(2, g_low_buffer, INDICATOR_DATA) ||
!SetIndexBuffer(3, g_close_buffer, INDICATOR_DATA) ||
!SetIndexBuffer(4, g_color_buffer, INDICATOR_COLOR_INDEX))
return INIT_FAILED;
ArraySetAsSeries(g_open_buffer, true);
ArraySetAsSeries(g_high_buffer, true);
ArraySetAsSeries(g_low_buffer, true);
ArraySetAsSeries(g_close_buffer, true);
ArraySetAsSeries(g_color_buffer, true);
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
// Keep the Renko bricks visible, but hide OHLC values from the Data Window.
PlotIndexSetInteger(0, PLOT_SHOW_DATA, false);
PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 2);
PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, InpUpColor);
PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, InpDownColor);
IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
if(!IndicatorSetInteger(INDICATOR_FIXED_MINIMUM, true) ||
!IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM, true))
{
Print("Renko5PointsMoscow: failed to enable linked fixed price scale.");
return INIT_FAILED;
}
g_brick_size = (double)InpBrickPoints * _Point;
const string short_name = StringFormat("Renko %d points / 00:03 Moscow", InpBrickPoints);
IndicatorSetString(INDICATOR_SHORTNAME, short_name);
long event_flag = 0;
if(ChartGetInteger(0, CHART_EVENT_MOUSE_MOVE, 0, event_flag))
g_mouse_move_previous = (event_flag != 0);
if(ChartGetInteger(0, CHART_EVENT_MOUSE_WHEEL, 0, event_flag))
g_mouse_wheel_previous = (event_flag != 0);
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
g_view_end = -1;
CreateAlertButton();
ChartRedraw(0);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(g_alert_button_name != "")
ObjectDelete(0, g_alert_button_name);
if(g_brick_object_prefix != "")
ObjectsDeleteAll(0, g_brick_object_prefix);
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, g_mouse_move_previous);
ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, g_mouse_wheel_previous);
}
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_CHART_CHANGE)
{
RenderPixelBricks();
return;
}
const string short_name = StringFormat("Renko %d points / 00:03 Moscow", InpBrickPoints);
const int subwindow = ChartWindowFind(0, short_name);
if(subwindow < 1)
return;
long height_value = 0;
long y_offset_value = 0;
if(!ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, subwindow, height_value) ||
!ChartGetInteger(0, CHART_WINDOW_YDISTANCE, subwindow, y_offset_value))
return;
const int window_top = (int)y_offset_value;
const int window_bottom = window_top + (int)height_value;
if(id == CHARTEVENT_MOUSE_MOVE)
{
const int x = (int)lparam;
const int y = (int)dparam;
const int state = (int)StringToInteger(sparam);
const bool left_down = ((state & 1) != 0);
const bool ctrl_down = ((state & 8) != 0);
const bool inside = (y >= window_top && y <= window_bottom);
if(!left_down || !inside)
{
g_drag_last_x = -1;
g_drag_remainder_x = 0.0;
return;
}
if(g_drag_last_x >= 0 && !ctrl_down)
{
const int dx = x - g_drag_last_x;
g_drag_remainder_x += (double)dx;
const int steps = (int)(g_drag_remainder_x / g_cell_width);
if(steps != 0)
{
if(g_view_end < 0)
g_view_end = g_brick_count;
const int current_chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
const int max_visible = GetVisibleBrickCapacity(current_chart_width, g_cell_width);
g_view_end -= steps;
if(g_view_end < max_visible)
g_view_end = max_visible;
if(g_view_end > g_brick_count)
g_view_end = g_brick_count;
g_drag_remainder_x -= (double)(steps * g_cell_width);
RenderPixelBricks();
}
}
g_drag_last_x = x;
return;
}
if(id == CHARTEVENT_MOUSE_WHEEL)
{
const int keymask = (int)(lparam >> 32);
const int mouse_x = (short)lparam;
const int mouse_y = (short)(lparam >> 16);
const int delta = (int)dparam;
if(mouse_y < window_top || mouse_y > window_bottom || delta == 0)
return;
// Vertical zoom is disabled because Renko follows the main chart's price range.
if((keymask & 8) != 0)
return;
const int old_width = g_cell_width;
const int new_width = (int)MathMax(5, MathMin(48, old_width + (delta > 0 ? 2 : -2)));
if(new_width != old_width)
{
const int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
const int old_visible = GetVisibleBrickCapacity(chart_width, old_width);
const int new_visible = GetVisibleBrickCapacity(chart_width, new_width);
if(g_view_end < 0)
g_view_end = g_brick_count;
const int left_margin = 8;
const int right_gap = GetRightEdgeGapPixels(chart_width);
const int usable_width = (int)MathMax(1, chart_width - left_margin - right_gap);
const double relative_x = MathMax(0.0, MathMin(1.0,
(double)(mouse_x - left_margin) / usable_width));
const double anchor_from_right = (1.0 - relative_x) * old_visible;
g_cell_width = new_width;
const int new_anchor_from_right = (int)MathRound(anchor_from_right / old_visible * new_visible);
g_view_end += new_anchor_from_right - (int)MathRound(anchor_from_right);
g_view_end = (int)MathMax(1, MathMin(g_brick_count, g_view_end));
}
RenderPixelBricks();
return;
}
if(id == CHARTEVENT_OBJECT_CLICK && sparam == g_alert_button_name)
{
g_alert_enabled = !g_alert_enabled;
ObjectSetString(0, g_alert_button_name, OBJPROP_TEXT,
(g_alert_enabled ? "Alert: ON" : "Alert: OFF"));
ObjectSetInteger(0, g_alert_button_name, OBJPROP_STATE, false);
ChartRedraw(0);
}
}
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(rates_total <= 0)
return 0;
const bool follow_latest = (g_view_end < 0 || g_view_end >= g_brick_count);
if(g_alert_button_name == "")
CreateAlertButton();
if(!g_history_initialized)
{
if(RebuildHistory())
g_history_initialized = true;
}
MqlTick latest_tick;
if(SymbolInfoTick(_Symbol, latest_tick) && latest_tick.bid > 0.0)
{
if(g_last_tick_msc == 0)
{
ProcessBid((datetime)latest_tick.time, latest_tick.bid, true);
g_last_tick_msc = latest_tick.time_msc;
g_last_bid = latest_tick.bid;
}
else if(latest_tick.time_msc > g_last_tick_msc)
{
MqlTick new_ticks[];
ResetLastError();
const int copied = CopyTicksRange(_Symbol, new_ticks, COPY_TICKS_INFO,
(ulong)(g_last_tick_msc + 1),
(ulong)latest_tick.time_msc);
if(copied > 0)
{
for(int i = 0; i < copied; ++i)
{
if(new_ticks[i].time_msc <= g_last_tick_msc || new_ticks[i].bid <= 0.0)
continue;
ProcessBid((datetime)new_ticks[i].time, new_ticks[i].bid, true);
g_last_tick_msc = new_ticks[i].time_msc;
g_last_bid = new_ticks[i].bid;
}
}
else
{
ProcessBid((datetime)latest_tick.time, latest_tick.bid, true);
g_last_tick_msc = latest_tick.time_msc;
g_last_bid = latest_tick.bid;
}
}
}
// DRAW_NONE is used; visible bricks are rendered by chart objects.
// Do not clear and refill unused indicator buffers on every market tick.
if(follow_latest)
g_view_end = g_brick_count;
double linked_min = 0.0;
double linked_max = 0.0;
bool linked_scale_changed = false;
UpdateLinkedPriceScale(linked_min, linked_max, linked_scale_changed);
long current_width = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
long current_height = 0;
const int current_subwindow = ChartWindowFind(0, StringFormat("Renko %d points / 00:03 Moscow", InpBrickPoints));
if(current_subwindow >= 1)
ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, current_subwindow, current_height);
if(linked_scale_changed ||
g_last_rendered_brick_count != g_brick_count ||
g_last_chart_width != (int)current_width ||
g_last_subwindow_height != (int)current_height)
RenderPixelBricks();
return rates_total;
}