328 Zeilen
12 KiB
MQL5
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();
|
|
}
|
|
//+------------------------------------------------------------------+
|