mql5/Shared Projects/ERMT-ML/Modules-ML8x/OrderFlowAnalyzer.mqh

777 lines
25 KiB
MQL5
Raw Permalink Normal View History

Module Integration Summary for External Trade Management Overview To fully integrate the enhanced external trade management system, updates are required to 5 out of 7 existing modules. The updates maintain backward compatibility while adding new functionality for external trade handling. Module Update Requirements 🟢 No Updates Required (2 modules) TechnicalAnalysis.mqh - Already provides necessary calculations EntrySystem.mqh - Only handles EA's own entry signals 🟡 Minor Updates (2 modules) DataTypes.mqh - Add external trade structures and fields Utilities.mqh - Enhanced logging for external trades 🟠 Moderate Updates (3 modules) RiskManager.mqh - Enhanced risk enforcement methods TradeManager.mqh - Improved stop management for externals Dashboard.mqh - Display external trade information Integration Steps Phase 1: Data Structures (DataTypes.mqh) Add ENUM_EXTERNAL_STATUS enumeration Extend ManagedTrade structure with external-specific fields Add ExternalTradeStats structure for metrics Update DashboardConfig with show_external flag Key additions: external_status - Track state of external trade source_name - Identify where trade came from stops_modified - Track if we modified the trade original_sl/tp - Store original values for comparison Phase 2: Risk Management (RiskManager.mqh) Add EnforceRiskRulesEnhanced() method Implement GetExternalExposure() for risk aggregation Add UpdateExternalStats() for tracking Enhance ValidateAndAdjustRiskExternal() method Key features: Separate risk calculation for external trades Cache mechanism for performance Statistical tracking of external positions Smart risk adjustment without closing trades Phase 3: Trade Management (TradeManager.mqh) Add ApplyDefaultStopsEnhanced() with better logic Implement OverrideExternalStops() with smart override Create ManageExternalTrade() with different rules Add ApplyBreakevenExternal() with wider triggers Key features: Smart stop override (only improve, never worsen) Different management rules for external trades Respect minimum broker distances Track modification success/failure rates Phase 4: User Interface (Dashboard.mqh) Add CreateExternalSection() for display area Implement UpdateExternalSection() for real-time updates Add SetCustomText() for flexible display Create ShowExternalTrades() toggle method Key features: Real-time external trade count and risk Color-coded risk warnings List of active external positions Modification statistics display Phase 5: Logging (Utilities.mqh) Add LogExternalTrade() for detailed event logging Create separate CSV log for external trades Enhance GenerateReportEnhanced() with external section Add IdentifyTradeSource() for magic number interpretation Key features: Separate CSV log for external trade events Detailed tracking of all modifications Source identification from magic numbers Enhanced reporting with external statistics
2025-08-27 14:21:02 +01:00
//+------------------------------------------------------------------+
//| OrderFlowAnalyzer.mqh |
//| Order Flow and Market Microstructure |
//| Volume Profile, Liquidity Detection, Flow Analysis |
//+------------------------------------------------------------------+
#ifndef ORDER_FLOW_ANALYZER_MQH
#define ORDER_FLOW_ANALYZER_MQH
#include "DataTypes_v71.mqh"
#include <Arrays/ArrayObj.mqh>
//+------------------------------------------------------------------+
//| Volume Profile Level |
//+------------------------------------------------------------------+
struct VolumeProfileLevel
{
double price;
double volume;
double buy_volume;
double sell_volume;
double delta;
int trades;
bool is_poc; // Point of Control
bool is_vah; // Value Area High
bool is_val; // Value Area Low
};
//+------------------------------------------------------------------+
//| Order Flow Imbalance |
//+------------------------------------------------------------------+
struct FlowImbalance
{
datetime time;
double bid_volume;
double ask_volume;
double imbalance_ratio;
double cumulative_delta;
double vpin; // Volume-synchronized PIN
bool absorption_detected;
};
//+------------------------------------------------------------------+
//| Large Order Detection |
//+------------------------------------------------------------------+
struct LargeOrder
{
datetime time;
double price;
double volume;
bool is_buy;
double impact; // Price impact
int execution_time_ms;
bool is_institutional;
};
//+------------------------------------------------------------------+
//| Order Flow Analyzer Class |
//+------------------------------------------------------------------+
class COrderFlowAnalyzer
{
private:
//--- Configuration
int m_profile_period;
double m_liquidity_threshold;
int m_large_order_multiple;
//--- Volume profile data
VolumeProfileLevel m_volume_profile[];
int m_profile_levels;
double m_poc_price;
double m_vah_price;
double m_val_price;
double m_vwap;
//--- Order flow tracking
FlowImbalance m_flow_history[];
int m_flow_buffer_size;
double m_cumulative_delta;
//--- Large order detection
LargeOrder m_large_orders[];
int m_large_order_count;
double m_avg_trade_size;
//--- Tick data buffer
MqlTick m_tick_buffer[];
int m_tick_count;
datetime m_last_update;
//--- Liquidity analysis
double m_bid_liquidity[];
double m_ask_liquidity[];
int m_liquidity_levels;
//--- Helper methods
void BuildVolumeProfile(string symbol);
void CalculateValueArea();
void DetectOrderAbsorption(MqlTick &tick);
void UpdateFlowImbalance(MqlTick &tick);
double CalculateVPIN(int lookback);
void AnalyzeLiquidity(string symbol);
public:
COrderFlowAnalyzer();
~COrderFlowAnalyzer();
//--- Initialization
bool Initialize(int profile_period, double liquidity_threshold);
//--- Real-time processing
void ProcessTick(string symbol, MqlTick &tick);
void UpdateProfile(string symbol);
//--- Volume profile analysis
double GetPOC() { return m_poc_price; }
double GetVAH() { return m_vah_price; }
double GetVAL() { return m_val_price; }
double GetVWAP() { return m_vwap; }
VolumeProfileLevel GetProfileLevel(double price);
//--- Order flow metrics
double GetFlowImbalance(string symbol);
double GetCumulativeDelta() { return m_cumulative_delta; }
double GetVPIN() { return CalculateVPIN(50); }
bool IsAbsorptionDetected();
//--- Large order detection
bool DetectInstitutionalFlow(string symbol);
int GetLargeOrderCount(bool buy_only = false);
double GetAverageTradeSize() { return m_avg_trade_size; }
//--- Liquidity analysis
double GetLiquidityScore(string symbol);
double GetBidDepth(int levels = 5);
double GetAskDepth(int levels = 5);
double GetSpreadQuality(string symbol);
//--- Signal generation
double GetSignalStrength(string symbol);
bool CheckLiquidityConditions(double min_liquidity);
//--- Market microstructure
double GetEffectiveSpread(string symbol);
double GetRealizedSpread(string symbol);
double GetPriceImpact(double volume, bool is_buy);
//--- Export data
void ExportVolumeProfile(string filename);
void ExportFlowData(string filename);
};
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
COrderFlowAnalyzer::COrderFlowAnalyzer()
{
m_profile_period = 20;
m_liquidity_threshold = 1000000;
m_large_order_multiple = 10;
m_profile_levels = 0;
m_poc_price = 0;
m_vah_price = 0;
m_val_price = 0;
m_vwap = 0;
m_flow_buffer_size = 1000;
m_cumulative_delta = 0;
m_large_order_count = 0;
m_avg_trade_size = 0;
m_tick_count = 0;
m_last_update = 0;
m_liquidity_levels = 10;
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
COrderFlowAnalyzer::~COrderFlowAnalyzer()
{
ArrayFree(m_volume_profile);
ArrayFree(m_flow_history);
ArrayFree(m_large_orders);
ArrayFree(m_tick_buffer);
ArrayFree(m_bid_liquidity);
ArrayFree(m_ask_liquidity);
}
//+------------------------------------------------------------------+
//| Initialize analyzer |
//+------------------------------------------------------------------+
bool COrderFlowAnalyzer::Initialize(int profile_period, double liquidity_threshold)
{
m_profile_period = profile_period;
m_liquidity_threshold = liquidity_threshold;
//--- Initialize arrays
ArrayResize(m_flow_history, m_flow_buffer_size);
ArrayResize(m_tick_buffer, 10000);
ArrayResize(m_large_orders, 1000);
ArrayResize(m_bid_liquidity, m_liquidity_levels);
ArrayResize(m_ask_liquidity, m_liquidity_levels);
Print("OrderFlowAnalyzer initialized: Period=", m_profile_period,
", LiquidityThreshold=", m_liquidity_threshold);
return true;
}
//+------------------------------------------------------------------+
//| Process incoming tick |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::ProcessTick(string symbol, MqlTick &tick)
{
//--- Store tick
if(m_tick_count < ArraySize(m_tick_buffer))
{
m_tick_buffer[m_tick_count] = tick;
m_tick_count++;
}
else
{
//--- Shift array and add new tick
ArrayCopy(m_tick_buffer, m_tick_buffer, 0, 1);
m_tick_buffer[ArraySize(m_tick_buffer)-1] = tick;
}
//--- Update flow imbalance
UpdateFlowImbalance(tick);
//--- Detect large orders
if(tick.volume_real > m_avg_trade_size * m_large_order_multiple)
{
LargeOrder order;
order.time = tick.time;
order.price = tick.last;
order.volume = tick.volume_real;
order.is_buy = (tick.last >= tick.ask); // Simplified
order.impact = 0; // Calculate later
order.execution_time_ms = (int)(tick.time_msc % 1000);
order.is_institutional = (tick.volume_real > m_liquidity_threshold / 100);
if(m_large_order_count < ArraySize(m_large_orders))
{
m_large_orders[m_large_order_count] = order;
m_large_order_count++;
}
}
//--- Update average trade size
m_avg_trade_size = m_avg_trade_size * 0.99 + tick.volume_real * 0.01;
//--- Detect absorption
DetectOrderAbsorption(tick);
//--- Update profile periodically
if(TimeCurrent() - m_last_update > 1)
{
UpdateProfile(symbol);
m_last_update = TimeCurrent();
}
}
//+------------------------------------------------------------------+
//| Build volume profile |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::BuildVolumeProfile(string symbol)
{
//--- Get price range
double high = 0, low = DBL_MAX;
double total_volume = 0;
double volume_price_sum = 0;
for(int i = 0; i < m_tick_count; i++)
{
if(m_tick_buffer[i].last > high) high = m_tick_buffer[i].last;
if(m_tick_buffer[i].last < low) low = m_tick_buffer[i].last;
total_volume += m_tick_buffer[i].volume_real;
volume_price_sum += m_tick_buffer[i].volume_real * m_tick_buffer[i].last;
}
if(high == low || total_volume == 0) return;
//--- Calculate VWAP
m_vwap = volume_price_sum / total_volume;
//--- Create price levels
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
int levels = (int)((high - low) / tick_size) + 1;
levels = MathMin(levels, 100); // Limit levels
ArrayResize(m_volume_profile, levels);
m_profile_levels = levels;
//--- Initialize levels
for(int i = 0; i < levels; i++)
{
m_volume_profile[i].price = low + i * tick_size;
m_volume_profile[i].volume = 0;
m_volume_profile[i].buy_volume = 0;
m_volume_profile[i].sell_volume = 0;
m_volume_profile[i].trades = 0;
m_volume_profile[i].is_poc = false;
m_volume_profile[i].is_vah = false;
m_volume_profile[i].is_val = false;
}
//--- Aggregate volume by price
double max_volume = 0;
int poc_index = 0;
for(int i = 0; i < m_tick_count; i++)
{
int level_index = (int)((m_tick_buffer[i].last - low) / tick_size);
if(level_index >= 0 && level_index < levels)
{
m_volume_profile[level_index].volume += m_tick_buffer[i].volume_real;
m_volume_profile[level_index].trades++;
//--- Determine buy/sell
if(m_tick_buffer[i].last >= m_tick_buffer[i].ask)
{
m_volume_profile[level_index].buy_volume += m_tick_buffer[i].volume_real;
}
else
{
m_volume_profile[level_index].sell_volume += m_tick_buffer[i].volume_real;
}
//--- Track POC
if(m_volume_profile[level_index].volume > max_volume)
{
max_volume = m_volume_profile[level_index].volume;
poc_index = level_index;
}
}
}
//--- Mark POC
m_volume_profile[poc_index].is_poc = true;
m_poc_price = m_volume_profile[poc_index].price;
//--- Calculate delta
for(int i = 0; i < levels; i++)
{
m_volume_profile[i].delta = m_volume_profile[i].buy_volume -
m_volume_profile[i].sell_volume;
}
//--- Calculate value area
CalculateValueArea();
}
//+------------------------------------------------------------------+
//| Calculate value area (70% of volume) |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::CalculateValueArea()
{
if(m_profile_levels == 0) return;
//--- Calculate total volume
double total_volume = 0;
for(int i = 0; i < m_profile_levels; i++)
{
total_volume += m_volume_profile[i].volume;
}
double value_area_volume = total_volume * 0.7;
double accumulated_volume = 0;
//--- Find POC index
int poc_index = -1;
for(int i = 0; i < m_profile_levels; i++)
{
if(m_volume_profile[i].is_poc)
{
poc_index = i;
accumulated_volume = m_volume_profile[i].volume;
break;
}
}
if(poc_index < 0) return;
//--- Expand from POC
int upper_index = poc_index;
int lower_index = poc_index;
while(accumulated_volume < value_area_volume &&
(upper_index < m_profile_levels - 1 || lower_index > 0))
{
double upper_volume = (upper_index < m_profile_levels - 1) ?
m_volume_profile[upper_index + 1].volume : 0;
double lower_volume = (lower_index > 0) ?
m_volume_profile[lower_index - 1].volume : 0;
if(upper_volume >= lower_volume && upper_index < m_profile_levels - 1)
{
upper_index++;
accumulated_volume += upper_volume;
}
else if(lower_index > 0)
{
lower_index--;
accumulated_volume += lower_volume;
}
}
//--- Mark value area
m_volume_profile[upper_index].is_vah = true;
m_volume_profile[lower_index].is_val = true;
m_vah_price = m_volume_profile[upper_index].price;
m_val_price = m_volume_profile[lower_index].price;
}
//+------------------------------------------------------------------+
//| Update flow imbalance |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::UpdateFlowImbalance(MqlTick &tick)
{
static int flow_index = 0;
FlowImbalance flow;
flow.time = tick.time;
//--- Classify as buy or sell based on tick
if(tick.last >= tick.ask)
{
flow.ask_volume = tick.volume_real;
flow.bid_volume = 0;
}
else if(tick.last <= tick.bid)
{
flow.bid_volume = tick.volume_real;
flow.ask_volume = 0;
}
else
{
//--- Split volume
double ratio = (tick.last - tick.bid) / (tick.ask - tick.bid);
flow.ask_volume = tick.volume_real * ratio;
flow.bid_volume = tick.volume_real * (1 - ratio);
}
//--- Calculate imbalance
double total = flow.bid_volume + flow.ask_volume;
if(total > 0)
{
flow.imbalance_ratio = (flow.ask_volume - flow.bid_volume) / total;
}
else
{
flow.imbalance_ratio = 0;
}
//--- Update cumulative delta
m_cumulative_delta += (flow.ask_volume - flow.bid_volume);
flow.cumulative_delta = m_cumulative_delta;
//--- Calculate VPIN
flow.vpin = CalculateVPIN(50);
//--- Store in buffer
m_flow_history[flow_index] = flow;
flow_index = (flow_index + 1) % m_flow_buffer_size;
}
//+------------------------------------------------------------------+
//| Calculate Volume-synchronized PIN |
//+------------------------------------------------------------------+
double COrderFlowAnalyzer::CalculateVPIN(int lookback)
{
if(m_tick_count < lookback) return 0;
double total_buy_volume = 0;
double total_sell_volume = 0;
int start_index = MathMax(0, m_tick_count - lookback);
for(int i = start_index; i < m_tick_count; i++)
{
if(m_tick_buffer[i].last >= m_tick_buffer[i].ask)
total_buy_volume += m_tick_buffer[i].volume_real;
else
total_sell_volume += m_tick_buffer[i].volume_real;
}
double total_volume = total_buy_volume + total_sell_volume;
if(total_volume > 0)
{
return MathAbs(total_buy_volume - total_sell_volume) / total_volume;
}
return 0;
}
//+------------------------------------------------------------------+
//| Detect order absorption |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::DetectOrderAbsorption(MqlTick &tick)
{
//--- Check recent flow history
int recent_count = MathMin(10, m_flow_buffer_size);
double recent_imbalance = 0;
double price_change = 0;
for(int i = 0; i < recent_count; i++)
{
int index = (m_flow_buffer_size + flow_index - i - 1) % m_flow_buffer_size;
recent_imbalance += m_flow_history[index].imbalance_ratio;
}
recent_imbalance /= recent_count;
//--- Check price movement
if(m_tick_count > 10)
{
price_change = (tick.last - m_tick_buffer[m_tick_count-10].last) /
m_tick_buffer[m_tick_count-10].last;
}
//--- Absorption detected if high volume but no price movement
bool absorption = (MathAbs(recent_imbalance) > 0.7 && MathAbs(price_change) < 0.0001);
if(absorption)
{
int current_index = (flow_index - 1 + m_flow_buffer_size) % m_flow_buffer_size;
m_flow_history[current_index].absorption_detected = true;
}
}
//+------------------------------------------------------------------+
//| Get flow imbalance |
//+------------------------------------------------------------------+
double COrderFlowAnalyzer::GetFlowImbalance(string symbol)
{
//--- Calculate recent flow imbalance
int lookback = MathMin(20, m_tick_count);
double total_buy = 0;
double total_sell = 0;
for(int i = m_tick_count - lookback; i < m_tick_count; i++)
{
if(i >= 0)
{
if(m_tick_buffer[i].last >= m_tick_buffer[i].ask)
total_buy += m_tick_buffer[i].volume_real;
else if(m_tick_buffer[i].last <= m_tick_buffer[i].bid)
total_sell += m_tick_buffer[i].volume_real;
}
}
double total = total_buy + total_sell;
if(total > 0)
return (total_buy - total_sell) / total;
return 0;
}
//+------------------------------------------------------------------+
//| Detect institutional flow |
//+------------------------------------------------------------------+
bool COrderFlowAnalyzer::DetectInstitutionalFlow(string symbol)
{
//--- Check recent large orders
int recent_large_orders = 0;
datetime current_time = TimeCurrent();
for(int i = 0; i < m_large_order_count; i++)
{
if(current_time - m_large_orders[i].time < 300) // Last 5 minutes
{
if(m_large_orders[i].is_institutional)
recent_large_orders++;
}
}
//--- Check cumulative volume
double recent_volume = 0;
int lookback = MathMin(100, m_tick_count);
for(int i = m_tick_count - lookback; i < m_tick_count; i++)
{
if(i >= 0)
recent_volume += m_tick_buffer[i].volume_real;
}
//--- Institutional flow detected if high volume and large orders
return (recent_large_orders >= 3 || recent_volume > m_liquidity_threshold);
}
//+------------------------------------------------------------------+
//| Get liquidity score |
//+------------------------------------------------------------------+
double COrderFlowAnalyzer::GetLiquidityScore(string symbol)
{
//--- Analyze bid/ask depth
double total_liquidity = 0;
//--- Estimate from recent volume and spread
double avg_spread = 0;
double avg_volume = 0;
int count = MathMin(100, m_tick_count);
for(int i = m_tick_count - count; i < m_tick_count; i++)
{
if(i >= 0)
{
avg_spread += (m_tick_buffer[i].ask - m_tick_buffer[i].bid);
avg_volume += m_tick_buffer[i].volume_real;
}
}
if(count > 0)
{
avg_spread /= count;
avg_volume /= count;
}
//--- Calculate liquidity score (0-100)
double spread_score = MathMax(0, 100 - avg_spread * 10000); // Tighter spread = higher score
double volume_score = MathMin(100, avg_volume / 1000); // Higher volume = higher score
double vpin_score = MathMax(0, 100 - GetVPIN() * 100); // Lower VPIN = higher score
return (spread_score + volume_score + vpin_score) / 3;
}
//+------------------------------------------------------------------+
//| Get signal strength based on order flow |
//+------------------------------------------------------------------+
double COrderFlowAnalyzer::GetSignalStrength(string symbol)
{
//--- Get flow imbalance
double imbalance = GetFlowImbalance(symbol);
//--- Check if at important levels
double current_price = SymbolInfoDouble(symbol, SYMBOL_BID);
double level_importance = 0;
if(MathAbs(current_price - m_poc_price) < SymbolInfoDouble(symbol, SYMBOL_POINT) * 10)
level_importance = 0.3; // At POC
else if(current_price >= m_val_price && current_price <= m_vah_price)
level_importance = 0.2; // In value area
else
level_importance = 0.1; // Outside value area
//--- Check for absorption
bool absorption = IsAbsorptionDetected();
double absorption_factor = absorption ? 1.5 : 1.0;
//--- Calculate signal strength
double strength = MathAbs(imbalance) * absorption_factor + level_importance;
//--- Boost for institutional flow
if(DetectInstitutionalFlow(symbol))
strength *= 1.3;
return MathMin(1.0, strength);
}
//+------------------------------------------------------------------+
//| Check if absorption detected |
//+------------------------------------------------------------------+
bool COrderFlowAnalyzer::IsAbsorptionDetected()
{
//--- Check recent flow history for absorption
int recent_count = MathMin(5, m_flow_buffer_size);
for(int i = 0; i < recent_count; i++)
{
int index = (m_flow_buffer_size + flow_index - i - 1) % m_flow_buffer_size;
if(m_flow_history[index].absorption_detected)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Update volume profile |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::UpdateProfile(string symbol)
{
BuildVolumeProfile(symbol);
AnalyzeLiquidity(symbol);
}
//+------------------------------------------------------------------+
//| Analyze market liquidity |
//+------------------------------------------------------------------+
void COrderFlowAnalyzer::AnalyzeLiquidity(string symbol)
{
//--- Simple liquidity estimation from volume profile
for(int i = 0; i < m_liquidity_levels; i++)
{
m_bid_liquidity[i] = 0;
m_ask_liquidity[i] = 0;
//--- Estimate liquidity at each level
if(i < m_profile_levels)
{
m_bid_liquidity[i] = m_volume_profile[i].sell_volume;
m_ask_liquidity[i] = m_volume_profile[i].buy_volume;
}
}
}
//+------------------------------------------------------------------+
//| Get effective spread |
//+------------------------------------------------------------------+
double COrderFlowAnalyzer::GetEffectiveSpread(string symbol)
{
if(m_tick_count < 2) return 0;
//--- Calculate average effective spread
double total_spread = 0;
int count = MathMin(100, m_tick_count - 1);
for(int i = m_tick_count - count; i < m_tick_count; i++)
{
if(i > 0)
{
double mid_price = (m_tick_buffer[i].bid + m_tick_buffer[i].ask) / 2;
double effective_spread = 2 * MathAbs(m_tick_buffer[i].last - mid_price);
total_spread += effective_spread;
}
}
return (count > 0) ? total_spread / count : 0;
}
//+------------------------------------------------------------------+
//| Get price impact estimate |
//+------------------------------------------------------------------+
double COrderFlowAnalyzer::GetPriceImpact(double volume, bool is_buy)
{
//--- Estimate price impact based on volume profile
double cumulative_volume = 0;
double weighted_price = 0;
double current_price = is_buy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) :
SymbolInfoDouble(_Symbol, SYMBOL_BID);
//--- Walk through profile levels
for(int i = 0; i < m_profile_levels; i++)
{
if(is_buy && m_volume_profile[i].price > current_price)
{
double available = m_volume_profile[i].sell_volume;
double take = MathMin(available, volume - cumulative_volume);
weighted_price += m_volume_profile[i].price * take;
cumulative_volume += take;
if(cumulative_volume >= volume) break;
}
else if(!is_buy && m_volume_profile[i].price < current_price)
{
double available = m_volume_profile[i].buy_volume;
double take = MathMin(available, volume - cumulative_volume);
weighted_price += m_volume_profile[i].price * take;
cumulative_volume += take;
if(cumulative_volume >= volume) break;
}
}
if(cumulative_volume > 0)
{
double avg_price = weighted_price / cumulative_volume;
return MathAbs(avg_price - current_price) / current_price * 100; // Percentage impact
}
return 0;
}
#endif // ORDER_FLOW_ANALYZER_MQH