If you've spent any time in the pro trader circles, you've heard the gospel of Order Flow and Volume Profile. The narrative is always the same: you need tick data, you need a Bloomberg terminal, you need a PhD in data science. Meanwhile, us retail traders stuck with MT4 and standard historical data are told we're just gambling. I call bullshit.
We don't have tick data, but we have Volume. And Volume, when you squint at it right, tells a hell of a story. This article is about squeezing that story out of MQL4's humble
iVolume() function. We're building a Volume Profile EA that doesn't just look at price levels; it looks at where the volume is concentrated and how that concentration shifts. And we're doing it without a single tick of bid/ask data.The Theory: Value Area and POC
For the uninitiated, Volume Profile (or Market Profile) displays trading activity at specific price levels over a given period. The key concepts are:
The core philosophy is that price tends to gravitate towards the POC. A breakout above the Value Area High (VAH) is significant, but a rejection from the POC is equally powerful. In conventional trading, we look for breakouts. In Volume Profile trading, we look for rotations around the POC.
Reference: This methodology is deeply rooted in the work of Peter Steidlmayer, the originator of Market Profile, as documented in his works and the CBOT's early educational materials on auction market theory.
The MQL4 Implementation Trap
Here's the first major gotcha most developers miss. When you use
iVolume(NULL, 0, i) in MQL4, you're getting the total tick count for that bar. That's great. But the standard MQL4 iCustom() function for building indicators is notoriously slow and clunky for complex calculations.We're not going to build an indicator and then call it. We're going to build the entire profile calculation inside the EA's
OnTick() or OnTimer(), storing the profile in a dynamic array. This gives us full control and avoids the performance bottleneck of custom indicator buffers.Here's a naive way to calculate POC:
``
mql4
//+------------------------------------------------------------------+
//| Calculate POC and Value Area |
//+------------------------------------------------------------------+
void CalculateProfile(int start_bar, int end_bar, double &levels[], double &volumes[]) {
ArrayResize(levels, 0);
ArrayResize(volumes, 0);
// Step 1: Collect unique price levels and their volumes
for(int bar = start_bar; bar >= end_bar; bar--) {
double price = (High[bar] + Low[bar]) / 2.0; // Using average as representative level
// WARNING: This is the weakness. Using (H+L)/2 is a simplification.
// It doesn't capture intra-bar range.
int index = -1;
for(int i = 0; i < ArraySize(levels); i++) {
if(MathAbs(levels[i] - price) < Point 0.5) {
index = i;
break;
}
}
if(index == -1) {
ArrayResize(levels, ArraySize(levels) + 1);
ArrayResize(volumes, ArraySize(volumes) + 1);
levels[ArraySize(levels)-1] = price;
volumes[ArraySize(volumes)-1] = Volume[bar];
} else {
volumes[index] += Volume[bar];
}
}
// Step 2: Find POC (index with max volume)
int poc_index = -1;
double max_vol = 0;
for(int i = 0; i < ArraySize(volumes); i++) {
if(volumes[i] > max_vol) {
max_vol = volumes[i];
poc_index = i;
}
}
// POC is at levels[poc_index]
}
`
See the problem? We're using (High + Low) / 2. This is a fatal flaw. A bar with a 100-point range and a bar with a 5-point range get reduced to a single point. We're effectively saying all volume happened at the middle, which is nonsense. This completely distorts the profile.
The TICK Data Workaround: Volume Distribution
We don't have ticks, but we can make an educated guess. Instead of assigning all volume to the midpoint, we distribute it across the bar's range using a simple assumption: volume is weighted more heavily towards the close of the bar*. This is based on the observation that institutional activity often intensifies near the close.
This is called Volume Weighted Average Price (VWAP) distribution, but applied to the profile. It's still an estimate, but it's a better estimate than the midpoint.
`mql4
//+------------------------------------------------------------------+
//| Improved Volume Distribution |
//+------------------------------------------------------------------+
void DistributeVolume(int bar, double &levels[], double &volumes[]) {
double high = High[bar];
double low = Low[bar];
double close = Close[bar];
double vol = Volume[bar];
// Define a simple triangular distribution where 60% of volume is near the close
double range = high - low;
if(range == 0) return;
// We'll create 5 sub-levels within the bar
double sub_levels[5];
double weights[5];
// Center the heavy weight around the close
for(int i = 0; i < 5; i++) {
double weight_factor = (i == 2) ? 0.4 : 0.15; // 40% at center, 15% each at edges
weights[i] = weight_factor;
// Position the sub-levels from low to high, but skewed towards close
double position = (double)i / 4.0; // 0.0 to 1.0
// Adjust position to be closer to the close
double close_pos = (close - low) / range;
double adjusted_pos = position close_pos + (1 - position) 0.5; // blend
sub_levels[i] = low + adjusted_pos range;
}
// Add distributed volume to the main profile arrays
for(int i = 0; i < 5; i++) {
double price_level = NormalizeDouble(sub_levels[i], Digits);
double vol_fraction = vol weights[i];
// Find or create level
int idx = -1;
for(int j = 0; j < ArraySize(levels); j++) {
if(MathAbs(levels[j] - price_level) < Point * 0.1) {
idx = j;
break;
}
}
if(idx == -1) {
ArrayResize(levels, ArraySize(levels) + 1);
ArrayResize(volumes, ArraySize(volumes) + 1);
levels[ArraySize(levels)-1] = price_level;
volumes[ArraySize(volumes)-1] = vol_fraction;
} else {
volumes[idx] += vol_fraction;
}
}
}
`
This is a massive improvement. It's not perfect, but it's a pragmatic solution that many professional retail quants use when tick data is unavailable.
The Holy Grail: Pseudo-Delta Divergence
Here's where we get to the good stuff. In professional order flow, "Delta" is the difference between buying and selling volume at each price. We can't measure that directly.
But we can calculate a Pseudo-Delta. Here's the logic:
If a bar closes above its midpoint, we assume buying pressure dominated. We label that bar's volume as "Buy Volume."
If a bar closes below its midpoint, we assume selling pressure dominated. We label it as "Sell Volume."
Then, for each price level in our profile, we sum the Buy Volume and Sell Volume from the bars that touched that level. The difference is our Pseudo-Delta.
Why is this powerful? When price approaches a historical POC level but the Pseudo-Delta at that level is showing a divergence (e.g., price is near POC, but Pseudo-Delta is becoming increasingly negative), it suggests that the "smart money" is not defending that level, and a breakdown is imminent.
This is my go-to edge. It's an original filter that I haven't seen published elsewhere. Most retail traders look at price relative to POC. I look at the momentum of volume relative to POC.
`mql4
//+------------------------------------------------------------------+
//| Calculate Pseudo-Delta |
//+------------------------------------------------------------------+
void CalcPseudoDelta(int start_bar, int end_bar) {
ArrayResize(delta_levels, 0);
ArrayResize(delta_values, 0);
for(int bar = start_bar; bar >= end_bar; bar--) {
double mid = (High[bar] + Low[bar]) / 2.0;
bool is_buy = (Close[bar] > mid); // Simplified assumption
double vol = Volume[bar];
// Distribute this volume across the bar's range (similar to DistributeVolume)
// ... and add to delta_levels with positive sign for buy, negative for sell.
}
// After aggregation, delta_values at a level represents Net Volume at that price.
}
`
The Full EA Framework
Here's how we put it all together in a usable EA. The EA runs on a daily timer, calculates the profile for the previous day's session, and then trades based on the current price's position relative to the VAH, VAL, and POC, filtered by the Pseudo-Delta divergence.
`mql4
//+------------------------------------------------------------------+
//| VolumeEA.mq4 |
//+------------------------------------------------------------------+
#property copyright "FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
extern int ProfileBars = 100; // Number of bars for profile
extern double RiskPercent = 1.0; // Risk per trade
extern int MagicNumber = 202404;
double POC_Level = 0;
double VAH_Level = 0;
double VAL_Level = 0;
double PseudoDelta_POC = 0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit() {
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick() {
static datetime last_calc = 0;
// Recalculate profile once per hour to save CPU
if(TimeCurrent() - last_calc >= 3600) {
CalculateDailyProfile();
last_calc = TimeCurrent();
}
// Check for entry signals
if(!IsNewBar()) return;
double current_price = Ask;
bool is_above_VAH = (current_price > VAH_Level);
bool is_below_VAL = (current_price < VAL_Level);
bool near_POC = (MathAbs(current_price - POC_Level) < (VAH_Level - VAL_Level) 0.2);
// Unique signal: Pseudo-Delta Divergence at POC
if(near_POC) {
// Get current Pseudo-Delta at POC level (calculated in the profile)
double current_delta = GetDeltaAtLevel(POC_Level);
// If Pseudo-Delta is significantly negative and price is above POC, it's a sell signal
if(current_delta < -Volume[0] 0.3 && current_price > POC_Level) {
OpenSell();
}
// If Pseudo-Delta is significantly positive and price is below POC, it's a buy signal
if(current_delta > Volume[0] * 0.3 && current_price < POC_Level) {
OpenBuy();
}
}
// Traditional breakout signals (with delta confirmation)
if(is_above_VAH && GetDeltaAtLevel(VAH_Level) > 0) {
// Break above VAH with positive delta = strong breakout
OpenBuy();
}
if(is_below_VAL && GetDeltaAtLevel(VAL_Level) < 0) {
OpenSell();
}
}
//+------------------------------------------------------------------+
//| Calculate Daily Profile |
//+------------------------------------------------------------------+
void CalculateDailyProfile() {
// This function populates POC_Level, VAH_Level, VAL_Level
// and the delta array using the logic described above.
// Implementation details omitted for brevity but follows the code structure.
}
`
The Hidden Killer: Non-Standard Trading Sessions
Here's the technical detail that will sink your EA if you ignore it. The profile calculation assumes a fixed session (e.g., the "daily" session from 00:00 to 23:59). But if you're trading on a 24/5 market like Forex, the "session" concept is fuzzy. If you recalc the profile at 00:00 server time, you're mixing the Asian, London, and NY sessions if they fall within that period.
My Solution: Instead of recalculating at midnight, recalc based on trading volume intervals. Find the period when the 1-hour volume consistently drops to its lowest (typically around 22:00-23:00 GMT). Use that as your "session boundary." This dynamically defines the profile period and makes your EA more robust across different brokers with different server times.
Reference: This approach aligns with the concepts discussed in the Bank for International Settlements (BIS) quarterly reviews, which highlight the distinct characteristics of the three major Forex trading sessions.
A Word on Performance
All this dynamic array resizing in OnTick() is going to kill your CPU if you're not careful. In the code above, we recalc every hour. That's fine. But the DistributeVolume function loops through arrays and can have O(n^2) complexity if not optimized.
Optimization Trick: Sort the levels array after each calculation. This allows you to use a binary search to find or create levels instead of a linear search. It reduces the complexity from O(n^2) to O(n log n). Here's a quick and dirty sort:
`mql4
void SortLevels(double &levels[], double &volumes[]) {
// Simple bubble sort for demonstration
int size = ArraySize(levels);
for(int i = 0; i < size - 1; i++) {
for(int j = 0; j < size - i - 1; j++) {
if(levels[j] > levels[j + 1]) {
double temp_level = levels[j];
double temp_vol = volumes[j];
levels[j] = levels[j + 1];
volumes[j] = volumes[j + 1];
levels[j + 1] = temp_level;
volumes[j + 1] = temp_vol;
}
}
}
}
`
The Bottom Line
This EA is not a black-box "set and forget" system. It's a framework that requires interpretation. The Pseudo-Delta divergence filter is my original contribution to this space. It's not a holy grail, but it gives you a probabilistic edge. You're no longer just looking at price; you're looking at the conviction behind price movements.
The code is a starting point. You'll need to tweak the distribution model, the session boundaries, and the delta threshold based on the volatility of your specific instrument. But once you do, you'll find that the humble iVolume()` function in MQL4 is one of the most underutilized weapons in a retail trader's arsenal.本文首发于FXEAR.com,原创内容,未经授权禁止转载。