mql5/Indicators/OrderFlowHistogram.mq5
abel324d f70b2d06fd
2026-02-17 13:29:33 +03:00

328 Zeilen
12 KiB
MQL5

//+------------------------------------------------------------------+
//| OrderFlowHistogram.mq5 |
//| Copyright 2026, Antigravity AI |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Antigravity AI"
#property link "https://www.mql5.com"
#property version "1.20"
#property indicator_chart_window
#property indicator_plots 0
#include <Canvas\Canvas.mqh>
//--- Input Parameters
input int InpPriceStepTicks = 5; // Price Clustering (Ticks per Row)
input int InpMaxBars = 30; // Max Bars with History (Limit for performance)
input color InpBuyColor = clrAqua;
input color InpSellColor = clrMagenta;
input int InpOpacity = 120; // Transparency (0-255)
input int InpHistWidthPct = 40; // Histogram Width (% of Bar Width)
input int InpFontSize = 9; // Label Font Size
input color InpTextColor = clrWhite; // Label Color
//--- Structures
struct LevelData
{
double price;
double buy_vol;
double sell_vol;
};
struct BarData
{
datetime time;
LevelData levels[];
int levels_count;
bool processed;
};
//--- Global Variables
CCanvas m_canvas;
BarData m_bars[];
int m_max_bars = 0;
double m_point_step = 0;
double m_prev_bid = 0;
double m_prev_ask = 0;
double m_prev_price = 0;
bool m_history_loaded = false;
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
m_max_bars = InpMaxBars;
ArrayResize(m_bars, m_max_bars);
for(int i=0; i<m_max_bars; i++)
{
m_bars[i].time = 0;
m_bars[i].levels_count = 0;
m_bars[i].processed = false;
ArrayResize(m_bars[i].levels, 0);
}
m_point_step = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * InpPriceStepTicks;
if(m_point_step <= 0) m_point_step = _Point * InpPriceStepTicks;
if(!m_canvas.CreateBitmapLabel("OFH_Canvas", 0, 0, (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS), COLOR_FORMAT_ARGB_NORMALIZE))
{
Print("Canvas Error: ", GetLastError());
return(INIT_FAILED);
}
m_canvas.Erase(0);
m_canvas.Update();
IndicatorSetString(INDICATOR_SHORTNAME, "OrderFlowHist v1.2");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
m_canvas.Destroy();
Comment("");
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
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 < 2) return(0);
// 1. Load History once
if(!m_history_loaded)
{
ProcessHistory(time, rates_total);
m_history_loaded = true;
}
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick)) return(rates_total);
datetime bar_time = time[rates_total-1];
// Handle New Bar or Shift
UpdateBarHistory(bar_time);
// Process Live Tick
ProcessSingleTick(tick, 0); // Always index 0 for live bar
// Render
Render();
return(rates_total);
}
//+------------------------------------------------------------------+
//| Process History Ticks for Existing Bars |
//+------------------------------------------------------------------+
void ProcessHistory(const datetime &time[], int rates_total)
{
Print("OrderFlow: Loading historical ticks for ", m_max_bars, " bars...");
for(int i = 0; i < m_max_bars; i++)
{
int bar_idx = rates_total - 1 - i;
if(bar_idx < 0) break;
datetime start = time[bar_idx];
datetime end = (bar_idx + 1 < rates_total) ? time[bar_idx + 1] - 1 : TimeCurrent();
m_bars[i].time = start;
MqlTick ticks[];
int copied = CopyTicksRange(_Symbol, ticks, COPY_TICKS_ALL, start * 1000, end * 1000);
if(copied > 0)
{
for(int j = 0; j < copied; j++)
{
ProcessSingleTick(ticks[j], i);
}
m_bars[i].processed = true;
}
}
Print("OrderFlow: History load complete.");
}
//+------------------------------------------------------------------+
//| Process a Single Tick into a specific Bar index |
//+------------------------------------------------------------------+
void ProcessSingleTick(MqlTick &tick, int bar_history_idx)
{
double price = (tick.last != 0) ? tick.last : (tick.bid + tick.ask) / 2.0;
double vol = (tick.volume_real > 0) ? tick.volume_real : (double)tick.volume;
if(vol <= 0) vol = 1.0;
// FIXED Bitwise Check (reverted user's logical &&)
string dir = "NEUTRAL";
if((tick.flags & TICK_FLAG_BUY) != 0) dir = "BUY";
else if((tick.flags & TICK_FLAG_SELL) != 0) dir = "SELL";
else if(tick.last != 0)
{
if(tick.last >= tick.ask) dir = "BUY";
else if(tick.last <= tick.bid) dir = "SELL";
}
else
{
// Ask/Bid movement check (fallback for many FX brokers)
static double last_bid = 0, last_ask = 0;
if(tick.ask > last_ask && last_ask > 0) dir = "BUY";
else if(tick.bid < last_bid && last_bid > 0) dir = "SELL";
else if((tick.flags & TICK_FLAG_ASK) != 0) dir = "BUY";
else if((tick.flags & TICK_FLAG_BID) != 0) dir = "SELL";
last_ask = tick.ask;
last_bid = tick.bid;
}
double level_price = MathFloor(price / m_point_step) * m_point_step;
int idx = -1;
for(int i=0; i<m_bars[bar_history_idx].levels_count; i++)
{
if(MathAbs(m_bars[bar_history_idx].levels[i].price - level_price) < _Point/2.0)
{
idx = i;
break;
}
}
if(idx == -1)
{
idx = m_bars[bar_history_idx].levels_count;
m_bars[bar_history_idx].levels_count++;
ArrayResize(m_bars[bar_history_idx].levels, m_bars[bar_history_idx].levels_count);
m_bars[bar_history_idx].levels[idx].price = level_price;
m_bars[bar_history_idx].levels[idx].buy_vol = 0;
m_bars[bar_history_idx].levels[idx].sell_vol = 0;
}
if(dir == "BUY") m_bars[bar_history_idx].levels[idx].buy_vol += vol;
else if(dir == "SELL") m_bars[bar_history_idx].levels[idx].sell_vol += vol;
}
//+------------------------------------------------------------------+
//| Manage Bar History Shift |
//+------------------------------------------------------------------+
void UpdateBarHistory(datetime current_bar_time)
{
if(m_bars[0].time == current_bar_time) return;
// Shift history
for(int i = m_max_bars - 1; i > 0; i--)
{
m_bars[i].time = m_bars[i-1].time;
m_bars[i].levels_count = m_bars[i-1].levels_count;
m_bars[i].processed = m_bars[i-1].processed;
ArrayCopy(m_bars[i].levels, m_bars[i-1].levels);
}
m_bars[0].time = current_bar_time;
m_bars[0].levels_count = 0;
m_bars[0].processed = false;
ArrayResize(m_bars[0].levels, 0);
}
//+------------------------------------------------------------------+
//| Rendering Logic |
//+------------------------------------------------------------------+
void Render()
{
int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
if(m_canvas.Width() != chart_w || m_canvas.Height() != chart_h)
m_canvas.Resize(chart_w, chart_h);
m_canvas.Erase(0);
for(int i=0; i<m_max_bars; i++)
{
if(m_bars[i].time == 0 || m_bars[i].levels_count == 0) continue;
int x_center = 0, y_dummy = 0;
datetime time_val = m_bars[i].time;
if(!ChartTimePriceToXY(0, 0, time_val, 0, x_center, y_dummy)) continue;
int x2 = 0;
ChartTimePriceToXY(0, 0, time_val + PeriodSeconds(), 0, x2, y_dummy);
int bar_w = MathAbs(x2 - x_center);
int hist_w_total = (int)(bar_w * InpHistWidthPct / 100.0);
if(hist_w_total < 4) hist_w_total = 4;
// Find Max Volume for scaling
double max_v = 0.0001;
for(int j=0; j<m_bars[i].levels_count; j++)
{
if(m_bars[i].levels[j].buy_vol > max_v) max_v = m_bars[i].levels[j].buy_vol;
if(m_bars[i].levels[j].sell_vol > max_v) max_v = m_bars[i].levels[j].sell_vol;
}
// Draw levels
for(int j=0; j<m_bars[i].levels_count; j++)
{
int y_top, y_bottom;
if(!ChartTimePriceToXY(0, 0, time_val, m_bars[i].levels[j].price + m_point_step, x_center, y_top)) continue;
if(!ChartTimePriceToXY(0, 0, time_val, m_bars[i].levels[j].price, x_center, y_bottom)) continue;
int w_buy = (int)(hist_w_total * m_bars[i].levels[j].buy_vol / max_v);
uint clr_buy = ColorToARGB(InpBuyColor, (uchar)InpOpacity);
m_canvas.FillRectangle(x_center, y_top, x_center + w_buy, y_bottom, clr_buy);
int w_sell = (int)(hist_w_total * m_bars[i].levels[j].sell_vol / max_v);
uint clr_sell = ColorToARGB(InpSellColor, (uchar)InpOpacity);
m_canvas.FillRectangle(x_center - w_sell, y_top, x_center, y_bottom, clr_sell);
}
// --- Draw Volume Labels ---
double bar_buy = 0, bar_sell = 0;
double bar_high = -1, bar_low = 1000000;
for(int j=0; j<m_bars[i].levels_count; j++)
{
bar_buy += m_bars[i].levels[j].buy_vol;
bar_sell += m_bars[i].levels[j].sell_vol;
if(m_bars[i].levels[j].price > bar_high) bar_high = m_bars[i].levels[j].price;
if(m_bars[i].levels[j].price < bar_low) bar_low = m_bars[i].levels[j].price;
}
if(bar_high == -1) continue;
bar_high += m_point_step; // Account for the top of the highest level
int y_text_top, y_text_bottom;
if(ChartTimePriceToXY(0, 0, time_val, bar_high, x_center, y_text_top) &&
ChartTimePriceToXY(0, 0, time_val, bar_low, x_center, y_text_bottom))
{
m_canvas.FontSet("Trebuchet MS", -InpFontSize * 10, FW_NORMAL);
// Total Volume (Top)
string txt_total = StringFormat("%.0f", bar_buy + bar_sell);
m_canvas.TextOut(x_center, y_text_top - 15, txt_total, ColorToARGB(InpTextColor), TA_CENTER | TA_BOTTOM);
// Buy Volume (Bottom Right)
string txt_buy = StringFormat("%.0f", bar_buy);
m_canvas.TextOut(x_center + 5, y_text_bottom + 5, txt_buy, ColorToARGB(InpBuyColor), TA_LEFT | TA_TOP);
// Sell Volume (Bottom Left)
string txt_sell = StringFormat("%.0f", bar_sell);
m_canvas.TextOut(x_center - 5, y_text_bottom + 5, txt_sell, ColorToARGB(InpSellColor), TA_RIGHT | TA_TOP);
}
}
m_canvas.Update();
}
//+------------------------------------------------------------------+