854 lines
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;
| |||
}
|