| #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;
|
| }
|
| }
|
| }
|
|
|
| RenderBricks(rates_total);
|
| 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;
|
| }
|