Graph Theory: Network Flow of Commodities (Ford-Fulkerson Algorithm), Used as a Liquidity-Capacity Engine
Table of Contents
Introduction
Most traders using ICT concepts know how to spot a Fair Value Gap or an Order Block. They can identify liquidity pools sitting above equal highs or below equal lows. But knowing where these structures are is only half the battle. The real frustration comes at the moment of decision—price is sitting in a bullish FVG, there is a liquidity pool above, and the setup looks valid. Yet the trade fails. Not because the analysis was wrong, but because the market simply did not have enough structural momentum to reach the target. There was no way to measure whether the path was strong or weak. The trader had no filter. They took the trade on appearance alone, and the market stalled somewhere in the middle.
This is where the Ford-Fulkerson algorithm comes in. The algorithm does not predict direction—that remains the job of liquidity pools, Fair Value Gaps, and Order Blocks. It measures the path's capacity between the current price and the liquidity target. Each structure between the source and the target becomes a node in a directed graph. Each connection receives a capacity score based on volume, price reaction, distance, and structural quality. Ford-Fulkerson then finds the maximum flow through that network. If the path is strong, the trade is qualified. If there is a bottleneck—a weak link in the chain—the system flags it and the trade is skipped. The trader now has a quantitative filter beneath the ICT framework. It turns a visual setup measured into a decision.
System Overview
The system is built in layers, and each layer has a specific job. The first layer scans the chart and detects market structure—Swing Highs, Swing Lows, Fair Value Gaps, Order Blocks, and Liquidity Pools. These are not drawn for visual purposes. They are converted into nodes inside a directed graph. The second layer builds the connections between those nodes and assigns each connection a capacity score. That score is calculated from four factors: tick volume, the strength of prior price reactions, the distance of the move, and the quality of the structure type. Liquidity Pools score the highest. Order Blocks follow. Fair Value Gaps and Swings fill out the rest. Once the graph is built, the Ford-Fulkerson engine runs through it and returns a single number—the maximum flow from the current price to the nearest target liquidity pool.

The engine discards low-capacity paths and returns one number—the market’s ability to reach liquidity.
The system never trades on flow alone. Direction comes first. Price must be sitting inside a Fair Value Gap or respecting an Order Block, and a liquidity pool must exist on the other side of the move to draw price toward it. Only once those ICT conditions are met does the flow number become relevant. If the maximum flow clears the threshold, the trade is qualified and sizing is applied dynamically—stronger flow produces larger position size, weaker flow produces smaller size. If the flow falls short, or if a bottleneck is detected anywhere along the path, the trade is skipped entirely. The result is a two-stage decision process: structure sets the direction, and the network decides whether the market has enough capacity to follow through.

Structure sets the direction. The network decides the capacity. Only when both align—ICT conditions met AND flow clears the threshold—does a trade execute.
Getting Started
//+------------------------------------------------------------------+ //| Ford-Fulkerson.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/en/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/users/johnhlomohang/" #property version "1.00" #include <Trade\Trade.mqh> #include <Trade\PositionInfo.mqh> //+------------------------------------------------------------------+ //| INPUT PARAMETERS | //+------------------------------------------------------------------+ // --- Structure Detection --- input group "=== Structure Detection ===" input int SwingLookback = 5; // Swing High/Low: bars each side input int FVG_MinPips = 3; // Minimum FVG size in pips input int LiquidityLookback = 50; // Bars to scan for liquidity pools input double EqualLevelTolerance = 0.0003; // Equal highs/lows tolerance (price) // --- Ford-Fulkerson Engine --- input group "=== Flow Engine ===" input double FlowThreshold = 8.0; // Minimum max-flow to qualify trade input double WeakBottleneck = 3.0; // Bottleneck below this = avoid trade // --- Multi-Timeframe --- input group "=== Multi-Timeframe ===" input bool UseMTF = true; input ENUM_TIMEFRAMES TF_Fast = PERIOD_M5; input ENUM_TIMEFRAMES TF_Mid = PERIOD_M15; input ENUM_TIMEFRAMES TF_Slow = PERIOD_H1; input ENUM_TIMEFRAMES TF_Major = PERIOD_H4; input double MTF_Weight_Fast = 0.1; input double MTF_Weight_Mid = 0.2; input double MTF_Weight_Slow = 0.3; input double MTF_Weight_Major = 0.4; // --- Risk Management --- input group "=== Risk Management ===" input double BaseRiskPercent = 1.0; // Base risk % per trade input double MaxRiskPercent = 2.0; // Maximum risk % (at peak flow) input double PeakFlowRef = 20.0; // Flow value = MaxRisk input double SL_BufferPips = 5.0; // SL buffer in pips input double TP_RR_Ratio = 2.0; // Risk:Reward ratio // --- Execution --- input group "=== Execution ===" input ulong MagicNumber = 202501; input int MaxOpenTrades = 2; input int SlippagePoints = 30; input bool EnableLong = true; input bool EnableShort = true; // --- Capacity Scoring --- input group "=== Capacity Scoring ===" input double W_Volume = 1.0; input double W_Reaction = 1.0; input double W_Distance = 1.0; input double W_Structure = 1.0;
To get started, we define the core input parameters that control every major component of the Expert Advisor. The structure detection settings determine how Swing Highs, Swing Lows, Fair Value Gaps, and liquidity pools are identified on the chart. The Ford-Fulkerson flow engine inputs establish the minimum liquidity-capacity requirements needed to qualify a trade and filter out weak market paths. Multi-timeframe settings allow us to combine flow measurements from several timeframes using configurable weights, while the risk management section adjusts position sizing based on the calculated flow. Finally, the execution and scoring parameters control trade placement and exposure limits. They also set the weights used to calculate edge capacities (volume, reaction strength, distance, and structure quality).
//+------------------------------------------------------------------+ //| ENUMERATIONS & STRUCTURES | //+------------------------------------------------------------------+ enum NodeType { NODE_SWING_HIGH, NODE_SWING_LOW, NODE_LIQUIDITY_BUY, NODE_LIQUIDITY_SELL, NODE_FVG_BULL, NODE_FVG_BEAR, NODE_OB_BULL, NODE_OB_BEAR, NODE_SOURCE, NODE_SINK }; enum SignalDir { SIG_NONE, SIG_BUY, SIG_SELL }; struct GraphNode { int id; NodeType type; datetime time; double price; double priceHigh; double priceLow; }; struct GraphEdge { int fromNode; int toNode; double capacity; double flow; }; struct SwingNode { datetime time; double price; int strength; }; struct FVGNode { double upper; double lower; bool bullish; datetime time; }; struct OBNode { double high; double low; bool bullish; datetime time; }; struct LiquidityNode { double price; bool buySide; datetime time; }; //+------------------------------------------------------------------+ //| GLOBAL VARIABLES | //+------------------------------------------------------------------+ CTrade trade; int totalNodes = 0; int totalEdges = 0; GraphNode g_nodes[]; GraphEdge g_edges[]; SwingNode g_swingHighs[], g_swingLows[]; FVGNode g_fvgs[]; OBNode g_obs[]; LiquidityNode g_liq[]; //--- Flat 1D matrices — accessed via Idx(row,col,N) double g_capFlat[]; double g_flowFlat[]; int g_matrixN = 0; // current matrix dimension double g_pointValue; double g_pipValue; int g_digits; string g_symbol; //--- Inline index helper int Idx(int r, int c, int N) { return r * N + c; } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { g_symbol = _Symbol; g_digits = (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS); g_pointValue = SymbolInfoDouble(g_symbol, SYMBOL_POINT); g_pipValue = (g_digits == 3 || g_digits == 5) ? g_pointValue * 10.0 : g_pointValue; trade.SetExpertMagicNumber(MagicNumber); trade.SetDeviationInPoints(SlippagePoints); trade.SetTypeFilling(ORDER_FILLING_IOC); Print("Ford-Fulkerson Liquidity EA initialized | Symbol=", g_symbol, " | PipValue=", g_pipValue, " | FlowThreshold=", FlowThreshold); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { Comment(""); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBar = 0; datetime curBar = iTime(g_symbol, PERIOD_CURRENT, 0); if(curBar == lastBar) return; lastBar = curBar; if(CountOpenTrades() >= MaxOpenTrades) return; //--- Layer 1 — Detect structures DetectSwings(); DetectFVGs(); DetectOrderBlocks(); DetectLiquidityPools(); //--- Layers 2-6 — Build graph, run flow, get signal SignalDir signal = SIG_NONE; double maxFlow = 0.0; double entryPx = 0.0, slPx = 0.0, tpPx = 0.0; if(UseMTF) { double mtfFlow = ComputeMTFFlow(); if(mtfFlow > FlowThreshold) signal = GetDirectionalSignal(maxFlow, entryPx, slPx, tpPx); maxFlow = mtfFlow; } else { signal = BuildAndRunGraph(maxFlow, entryPx, slPx, tpPx); } //--- Layer 7 — Execute with dynamic sizing if(signal != SIG_NONE && maxFlow > FlowThreshold) { double riskPct = DynamicRisk(maxFlow); double lots = CalcLotSize(riskPct, MathAbs(entryPx - slPx)); if(signal == SIG_BUY && EnableLong) trade.Buy(lots, g_symbol, entryPx, slPx, tpPx, "FF_Buy|Flow=" + DoubleToString(maxFlow, 2)); else if(signal == SIG_SELL && EnableShort) trade.Sell(lots, g_symbol, entryPx, slPx, tpPx, "FF_Sell|Flow=" + DoubleToString(maxFlow, 2)); } ShowDashboard(signal, maxFlow); }
The EA is built around a set of enumerations, structures, and global variables that define the market structure graph. We use node types to represent Swing Highs, Swing Lows, liquidity pools, Fair Value Gaps, Order Blocks, including special SOURCE and SINK nodes required by the flow engine. Dedicated structures store the properties of each detected market feature, while graph nodes and edges hold the information needed for capacity and flow calculations. The global arrays maintain the graph data, capacity matrices, detected structures, and symbol-specific settings used throughout the EA.
The initialization routine loads the symbol properties, calculates pip values, and configures the trade execution settings. During each new candle, we detect market structures and convert them into a graph representation of the current market state. The flow engine then evaluates the available liquidity-capacity path and generates a directional signal when the calculated flow is strong enough. Position sizing is adjusted dynamically according to the flow strength, allowing larger exposure on stronger opportunities and smaller exposure on weaker ones. If all trading conditions are satisfied, the EA executes the trade and updates the dashboard with the latest flow and signal information.
//+------------------------------------------------------------------+ //| LAYER 1 — STRUCTURE DETECTION | //+------------------------------------------------------------------+ void DetectSwings() { ArrayResize(g_swingHighs, 0); ArrayResize(g_swingLows, 0); int bars = iBars(g_symbol, PERIOD_CURRENT); int limit = (int)MathMin(bars - SwingLookback - 1, 200); for(int i = SwingLookback; i < limit; i++) { double h = iHigh(g_symbol, PERIOD_CURRENT, i); double l = iLow(g_symbol, PERIOD_CURRENT, i); bool isHigh = true, isLow = true; for(int k = 1; k <= SwingLookback; k++) { if(iHigh(g_symbol,PERIOD_CURRENT,i-k) >= h || iHigh(g_symbol,PERIOD_CURRENT,i+k) >= h) isHigh = false; if(iLow(g_symbol,PERIOD_CURRENT,i-k) <= l || iLow(g_symbol,PERIOD_CURRENT,i+k) <= l) isLow = false; } if(isHigh) { int sz = ArraySize(g_swingHighs); ArrayResize(g_swingHighs, sz+1); g_swingHighs[sz].time = iTime(g_symbol,PERIOD_CURRENT,i); g_swingHighs[sz].price = h; g_swingHighs[sz].strength = SwingStrength(i); } if(isLow) { int sz = ArraySize(g_swingLows); ArrayResize(g_swingLows, sz+1); g_swingLows[sz].time = iTime(g_symbol,PERIOD_CURRENT,i); g_swingLows[sz].price = l; g_swingLows[sz].strength = SwingStrength(i); } } } //+------------------------------------------------------------------+ //| Swing Strength | //+------------------------------------------------------------------+ int SwingStrength(int bar) { double body = MathAbs(iOpen(g_symbol,PERIOD_CURRENT,bar) - iClose(g_symbol,PERIOD_CURRENT,bar)); double candle = iHigh(g_symbol,PERIOD_CURRENT,bar) - iLow(g_symbol,PERIOD_CURRENT,bar); if(candle < g_pipValue) return 1; double ratio = 1.0 - (body / candle); if(ratio > 0.7) return 3; if(ratio > 0.4) return 2; return 1; } //+------------------------------------------------------------------+ //| Detect Fair Value Gaps | //+------------------------------------------------------------------+ void DetectFVGs() { ArrayResize(g_fvgs, 0); int bars = iBars(g_symbol, PERIOD_CURRENT); int limit = (int)MathMin(bars - 2, 200); for(int i = 2; i < limit; i++) { double c1H = iHigh(g_symbol,PERIOD_CURRENT,i); double c1L = iLow(g_symbol,PERIOD_CURRENT,i); double c3H = iHigh(g_symbol,PERIOD_CURRENT,i-2); double c3L = iLow(g_symbol,PERIOD_CURRENT,i-2); if(c1H < c3L && (c3L - c1H) >= FVG_MinPips * g_pipValue) { int sz = ArraySize(g_fvgs); ArrayResize(g_fvgs, sz+1); g_fvgs[sz].upper = c3L; g_fvgs[sz].lower = c1H; g_fvgs[sz].bullish = true; g_fvgs[sz].time = iTime(g_symbol,PERIOD_CURRENT,i-1); } else if(c1L > c3H && (c1L - c3H) >= FVG_MinPips * g_pipValue) { int sz = ArraySize(g_fvgs); ArrayResize(g_fvgs, sz+1); g_fvgs[sz].upper = c1L; g_fvgs[sz].lower = c3H; g_fvgs[sz].bullish = false; g_fvgs[sz].time = iTime(g_symbol,PERIOD_CURRENT,i-1); } } } //+------------------------------------------------------------------+ //| Detect Order Blocks | //+------------------------------------------------------------------+ void DetectOrderBlocks() { ArrayResize(g_obs, 0); int bars = iBars(g_symbol, PERIOD_CURRENT); int limit = (int)MathMin(bars - 5, 200); for(int i = 3; i < limit; i++) { double displacement = MathAbs(iClose(g_symbol,PERIOD_CURRENT,i-2) - iOpen(g_symbol,PERIOD_CURRENT,i-2)); double avgCandle = AverageCandleSize(i, 10); if(displacement < avgCandle * 1.5) continue; int sz = ArraySize(g_obs); ArrayResize(g_obs, sz+1); g_obs[sz].high = iHigh(g_symbol,PERIOD_CURRENT,i); g_obs[sz].low = iLow(g_symbol,PERIOD_CURRENT,i); g_obs[sz].bullish = (iClose(g_symbol,PERIOD_CURRENT,i) < iOpen(g_symbol,PERIOD_CURRENT,i)); g_obs[sz].time = iTime(g_symbol,PERIOD_CURRENT,i); } } //+------------------------------------------------------------------+ //| Average Candle Size | //+------------------------------------------------------------------+ double AverageCandleSize(int startBar, int count) { double total = 0; for(int i = startBar; i < startBar + count; i++) total += iHigh(g_symbol,PERIOD_CURRENT,i) - iLow(g_symbol,PERIOD_CURRENT,i); return (count > 0) ? total / count : g_pipValue; } //+------------------------------------------------------------------+ //| Detect Liquidity Pools | //+------------------------------------------------------------------+ void DetectLiquidityPools() { ArrayResize(g_liq, 0); int limit = (int)MathMin(iBars(g_symbol,PERIOD_CURRENT), LiquidityLookback); for(int i = 1; i < limit; i++) { double hi = iHigh(g_symbol,PERIOD_CURRENT,i); double lo = iLow(g_symbol,PERIOD_CURRENT,i); for(int j = i+1; j < limit; j++) { double hj = iHigh(g_symbol,PERIOD_CURRENT,j); double lj = iLow(g_symbol,PERIOD_CURRENT,j); if(MathAbs(hi - hj) <= EqualLevelTolerance) { int sz = ArraySize(g_liq); ArrayResize(g_liq, sz+1); g_liq[sz].price = (hi + hj) * 0.5; g_liq[sz].buySide = true; g_liq[sz].time = iTime(g_symbol,PERIOD_CURRENT,i); } if(MathAbs(lo - lj) <= EqualLevelTolerance) { int sz = ArraySize(g_liq); ArrayResize(g_liq, sz+1); g_liq[sz].price = (lo + lj) * 0.5; g_liq[sz].buySide = false; g_liq[sz].time = iTime(g_symbol,PERIOD_CURRENT,i); } } } //--- Swing highs → buy-side liquidity; swing lows → sell-side for(int i = 0; i < ArraySize(g_swingHighs); i++) { int sz = ArraySize(g_liq); ArrayResize(g_liq, sz+1); g_liq[sz].price = g_swingHighs[i].price; g_liq[sz].buySide = true; g_liq[sz].time = g_swingHighs[i].time; } for(int i = 0; i < ArraySize(g_swingLows); i++) { int sz = ArraySize(g_liq); ArrayResize(g_liq, sz+1); g_liq[sz].price = g_swingLows[i].price; g_liq[sz].buySide = false; g_liq[sz].time = g_swingLows[i].time; } }
This section is responsible for detecting the market structures that will later become nodes within the Ford-Fulkerson liquidity network. We scan historical price data to identify Swing Highs and Swing Lows by comparing each candle against neighboring candles within the configured lookback range. When a valid swing point is found, its price, timestamp, and strength are stored for later use. The SwingStrength() function then evaluates the quality of each swing by comparing the candle body to its total range, allowing stronger rejection candles to receive higher strength values.
We also detect Fair Value Gaps and Order Blocks, which serve as key liquidity structures within the graph. A Fair Value Gap is identified when a price imbalance exists between non-adjacent candles and the gap exceeds the minimum size requirement. Bullish and bearish gaps are stored together with their boundaries and timestamps. Order Blocks are detected after significant displacement moves, where the displacement candle must be noticeably larger than the average recent candle size. This process helps filter out weak structures and retain only those associated with meaningful market momentum.
Liquidity pools are then identified by searching for equal highs and equal lows within the configured lookback window. When two levels fall within the allowed tolerance, they are treated as potential buy-side or sell-side liquidity and added to the liquidity array. The routine also converts previously detected Swing Highs into buy-side liquidity nodes and Swing Lows into sell-side liquidity nodes. By the end of this layer, we have swings, Fair Value Gaps, Order Blocks, and liquidity pools. These elements are then transformed into graph nodes for capacity and flow calculations.
//+------------------------------------------------------------------+ //| GRAPH BUILD + FORD-FULKERSON DISPATCH | //+------------------------------------------------------------------+ SignalDir BuildAndRunGraph(double &maxFlow, double &entry, double &sl, double &tp) { double curPrice = SymbolInfoDouble(g_symbol, SYMBOL_BID); LiquidityNode bullTarget, bearTarget; double bullFlow = 0.0, bearFlow = 0.0; if(FindNearestLiquidity(curPrice, true, bullTarget)) bullFlow = BuildDirectionalGraph(curPrice, bullTarget, true); if(FindNearestLiquidity(curPrice, false, bearTarget)) bearFlow = BuildDirectionalGraph(curPrice, bearTarget, false); bool bullSig = CheckBullEntry(curPrice); bool bearSig = CheckBearEntry(curPrice); if(bullSig && bullFlow >= bearFlow && bullFlow > FlowThreshold) { maxFlow = bullFlow; entry = SymbolInfoDouble(g_symbol, SYMBOL_ASK); sl = GetBullishSL(curPrice); tp = GetTP(entry, sl, true); return SIG_BUY; } if(bearSig && bearFlow > bullFlow && bearFlow > FlowThreshold) { maxFlow = bearFlow; entry = SymbolInfoDouble(g_symbol, SYMBOL_BID); sl = GetBearishSL(curPrice); tp = GetTP(entry, sl, false); return SIG_SELL; } maxFlow = MathMax(bullFlow, bearFlow); return SIG_NONE; } //+------------------------------------------------------------------+ //| Find Nearest Liquidity | //+------------------------------------------------------------------+ bool FindNearestLiquidity(double price, bool buySide, LiquidityNode &found) { double best = DBL_MAX; bool ok = false; int n = ArraySize(g_liq); for(int i = 0; i < n; i++) { if(g_liq[i].buySide != buySide) continue; double dist = buySide ? (g_liq[i].price - price) : (price - g_liq[i].price); if(dist > 0.0 && dist < best) { best = dist; found = g_liq[i]; ok = true; } } return ok; }
This section acts as the decision-making layer between market structure analysis and trade execution. We first locate the nearest buy-side and sell-side liquidity targets relative to the current market price, then build separate directional graphs to calculate the available liquidity-capacity flow toward each target using the Ford-Fulkerson engine. The resulting bullish and bearish flow values are combined with entry validation checks based on the detected market structures. A buy signal is generated when the bullish setup is valid, its flow exceeds the required threshold, and its capacity is greater than or equal to the bearish alternative. A sell signal follows the same logic in the opposite direction. When a valid signal is found, we calculate the entry price, stop-loss, and take-profit levels. FindNearestLiquidity() anchors the graph to the closest relevant liquidity pool that price can realistically target.
//+------------------------------------------------------------------+ //| DIRECTED GRAPH CONSTRUCTION | //+------------------------------------------------------------------+ double BuildDirectionalGraph(double srcPrice, LiquidityNode &sinkNode, bool bullish) { ArrayResize(g_nodes, 0); ArrayResize(g_edges, 0); totalNodes = 0; totalEdges = 0; //--- Source node AddNode(NODE_SOURCE, TimeCurrent(), srcPrice, srcPrice, srcPrice); //--- Intermediate structure nodes AddStructureNodes(srcPrice, sinkNode.price, bullish); //--- Sink node AddNode(bullish ? NODE_LIQUIDITY_BUY : NODE_LIQUIDITY_SELL, sinkNode.time, sinkNode.price, sinkNode.price, sinkNode.price); int sinkID = totalNodes - 1; if(sinkID < 1) return 0.0; //--- Sort + connect SortNodesByPrice(bullish); ConnectConsecutiveNodes(); //--- Build flat capacity matrix (N×N stored as 1D array) int N = totalNodes; g_matrixN = N; ArrayResize(g_capFlat, N * N); ArrayResize(g_flowFlat, N * N); ArrayFill(g_capFlat, 0, N * N, 0.0); ArrayFill(g_flowFlat, 0, N * N, 0.0); for(int e = 0; e < totalEdges; e++) { int f = g_edges[e].fromNode; int t = g_edges[e].toNode; g_capFlat[Idx(f, t, N)] += g_edges[e].capacity; } //--- Bottleneck safety if(MinEdgeCapacity() < WeakBottleneck) return 0.0; //--- Run Edmonds-Karp return EdmondsKarp(0, sinkID, N); } //+------------------------------------------------------------------+ //| Add Node | //+------------------------------------------------------------------+ void AddNode(NodeType type, datetime t, double price, double hi, double lo) { int sz = totalNodes; ArrayResize(g_nodes, sz + 1); g_nodes[sz].id = sz; g_nodes[sz].type = type; g_nodes[sz].time = t; g_nodes[sz].price = price; g_nodes[sz].priceHigh = hi; g_nodes[sz].priceLow = lo; totalNodes++; } //+------------------------------------------------------------------+ //| Add Structure Nodes | //+------------------------------------------------------------------+ void AddStructureNodes(double srcP, double sinkP, bool bullish) { double lo = MathMin(srcP, sinkP); double hi = MathMax(srcP, sinkP); for(int i = 0; i < ArraySize(g_fvgs); i++) { double mid = (g_fvgs[i].upper + g_fvgs[i].lower) * 0.5; if(mid < lo || mid > hi) continue; if(bullish && !g_fvgs[i].bullish) continue; if(!bullish && g_fvgs[i].bullish) continue; AddNode(bullish ? NODE_FVG_BULL : NODE_FVG_BEAR, g_fvgs[i].time, mid, g_fvgs[i].upper, g_fvgs[i].lower); } for(int i = 0; i < ArraySize(g_obs); i++) { double mid = (g_obs[i].high + g_obs[i].low) * 0.5; if(mid < lo || mid > hi) continue; if(bullish && !g_obs[i].bullish) continue; if(!bullish && g_obs[i].bullish) continue; AddNode(bullish ? NODE_OB_BULL : NODE_OB_BEAR, g_obs[i].time, mid, g_obs[i].high, g_obs[i].low); } if(bullish) { for(int i = 0; i < ArraySize(g_swingLows); i++) { double p = g_swingLows[i].price; if(p < lo || p > hi) continue; AddNode(NODE_SWING_LOW, g_swingLows[i].time, p, p, p); } } else { for(int i = 0; i < ArraySize(g_swingHighs); i++) { double p = g_swingHighs[i].price; if(p < lo || p > hi) continue; AddNode(NODE_SWING_HIGH, g_swingHighs[i].time, p, p, p); } } } //+------------------------------------------------------------------+ //| Sort Nodes | //+------------------------------------------------------------------+ void SortNodesByPrice(bool bullish) { //--- Insertion sort — ascending for bullish, descending for bearish for(int i = 1; i < totalNodes; i++) { GraphNode key = g_nodes[i]; int j = i - 1; while(j >= 0) { bool swap = bullish ? (g_nodes[j].price > key.price) : (g_nodes[j].price < key.price); if(!swap) break; g_nodes[j+1] = g_nodes[j]; j--; } g_nodes[j+1] = key; } for(int k = 0; k < totalNodes; k++) g_nodes[k].id = k; } //+------------------------------------------------------------------+ //| Connect Nodes | //+------------------------------------------------------------------+ void ConnectConsecutiveNodes() { for(int i = 0; i < totalNodes - 1; i++) { double cap = ComputeEdgeCapacity(g_nodes[i], g_nodes[i+1]); int sz = totalEdges; ArrayResize(g_edges, sz + 1); g_edges[sz].fromNode = i; g_edges[sz].toNode = i + 1; g_edges[sz].capacity = cap; g_edges[sz].flow = 0.0; totalEdges++; } }
Here, we construct the directional market graph that will be analyzed by the Ford-Fulkerson liquidity-capacity engine. We begin by clearing any previous graph data and creating a source node at the current market price. The EA then scans for relevant Fair Value Gaps, Order Blocks, and swing points located between the current price and the selected liquidity target. Only structures that align with the intended direction are included, ensuring that bullish graphs contain bullish structures while bearish graphs contain bearish structures. Once all valid structures have been collected, the target liquidity pool is added as the sink node, creating a complete path from the current market position to the projected liquidity objective.
The graph is then organized and prepared for flow analysis. All nodes are sorted by price so that the graph follows a logical progression toward the liquidity target, and consecutive nodes are connected with edges whose capacities are calculated from the strength of the underlying market structures. These capacities are stored in a matrix representation that can be processed efficiently by the flow engine. Before running the algorithm, the EA checks for weak bottlenecks that could indicate insufficient liquidity-capacity along the path. If the graph passes this validation step, the Edmonds-Karp implementation of Ford-Fulkerson is executed to calculate the maximum flow from the source node to the sink node, providing a quantitative measure of how much price-travel capacity exists within the detected market structure network.
//+------------------------------------------------------------------+ //| CAPACITY FORMULA | //+------------------------------------------------------------------+ double ComputeEdgeCapacity(GraphNode &from, GraphNode &to) { double avgVol = AverageVolume(20); double curVol = (double)iVolume(g_symbol, PERIOD_CURRENT, 1); double volScore = (avgVol > 0) ? MathMin(curVol / avgVol, 3.0) * 4.0 : 2.0; double priceDiff = MathAbs(to.price - from.price); double reactionPips = priceDiff / g_pipValue; double reactionScore = MathMin(reactionPips / 20.0, 1.0) * 5.0; double distScore = MathMin(priceDiff / (g_pipValue * 50.0), 1.0) * 4.0; double structScore = NodeStructureScore(to.type); double cap = W_Volume * volScore + W_Reaction * reactionScore + W_Distance * distScore + W_Structure* structScore; return MathMax(cap, 0.1); } //+------------------------------------------------------------------+ //| Node Structure Score | //+------------------------------------------------------------------+ double NodeStructureScore(NodeType t) { switch(t) { case NODE_LIQUIDITY_BUY: case NODE_LIQUIDITY_SELL: return 8.0; case NODE_OB_BULL: case NODE_OB_BEAR: return 5.0; case NODE_FVG_BULL: case NODE_FVG_BEAR: return 4.0; case NODE_SWING_HIGH: case NODE_SWING_LOW: return 3.0; default: return 1.0; } } //+------------------------------------------------------------------+ //| Average Volume | //+------------------------------------------------------------------+ double AverageVolume(int period) { double sum = 0; for(int i = 1; i <= period; i++) sum += (double)iVolume(g_symbol, PERIOD_CURRENT, i); return (period > 0) ? sum / period : 1.0; } //+------------------------------------------------------------------+ //| Minimum Edge Capacity | //+------------------------------------------------------------------+ double MinEdgeCapacity() { double minCap = DBL_MAX; for(int i = 0; i < totalEdges; i++) if(g_edges[i].capacity < minCap) minCap = g_edges[i].capacity; return (minCap == DBL_MAX) ? 0.0 : minCap; } //+------------------------------------------------------------------+ //| FORD-FULKERSON — EDMONDS-KARP (flat 1D matrix) | //+------------------------------------------------------------------+ double EdmondsKarp(int source, int sink, int N) { if(N < 2 || source == sink) return 0.0; //--- Build residual as flat 1D array double residual[]; ArrayResize(residual, N * N); for(int i = 0; i < N; i++) for(int j = 0; j < N; j++) residual[Idx(i,j,N)] = g_capFlat[Idx(i,j,N)]; double maxFlow = 0.0; while(true) { //--- BFS — find augmenting path int parent[]; double pathFlow[]; int bfsQueue[]; ArrayResize(parent, N); ArrayFill(parent, 0, N, -1); ArrayResize(pathFlow, N); ArrayFill(pathFlow, 0, N, 0.0); ArrayResize(bfsQueue, N); parent[source] = source; pathFlow[source] = 1e18; int qHead = 0, qTail = 0; bfsQueue[qTail++] = source; while(qHead < qTail && parent[sink] == -1) { int u = bfsQueue[qHead++]; for(int v = 0; v < N; v++) { if(parent[v] == -1 && residual[Idx(u,v,N)] > 1e-9) { parent[v] = u; pathFlow[v] = MathMin(pathFlow[u], residual[Idx(u,v,N)]); if(v == sink) break; if(qTail < N) bfsQueue[qTail++] = v; } } } if(parent[sink] == -1) break; // no augmenting path found double augFlow = pathFlow[sink]; maxFlow += augFlow; //--- Update residual along path int v = sink; while(v != source) { int u = parent[v]; residual[Idx(u,v,N)] -= augFlow; residual[Idx(v,u,N)] += augFlow; v = u; } } return maxFlow; }
This section determines the capacity of every edge within the market structure graph. We calculate a liquidity-capacity score by combining several factors that describe the quality of the path between two connected nodes. The volume component measures current market participation relative to recent activity, while the reaction component evaluates the price movement between structures. A distance score rewards larger and cleaner moves, and a structure score reflects the importance of the destination node. Liquidity pools receive the highest score because they represent the primary market objectives, followed by Order Blocks, Fair Value Gaps, and swing points. These individual components are weighted and combined to produce the final edge capacity used by the flow engine.
The second part implements the Edmonds-Karp variant of the Ford-Fulkerson algorithm to calculate the maximum liquidity-capacity flow through the graph. Before the calculation begins, the algorithm builds a residual network from the graph's capacity matrix and identifies the weakest edge through a bottleneck check. It then repeatedly uses Breadth-First Search to locate augmenting paths from the source node to the sink node. For each valid path, the algorithm determines how much additional flow can pass through the network and updates the residual capacities accordingly. This process continues until no further augmenting paths can be found, at which point the accumulated flow represents the maximum price-travel capacity available between the current market position and the selected liquidity target.
//+------------------------------------------------------------------+ //| ICT ENTRY CONDITIONS (DIRECTIONAL SIGNAL) | //+------------------------------------------------------------------+ bool CheckBullEntry(double price) { bool inFVG = false; for(int i = 0; i < ArraySize(g_fvgs); i++) if(g_fvgs[i].bullish && price >= g_fvgs[i].lower && price <= g_fvgs[i].upper) { inFVG = true; break; } bool nearOB = false; for(int i = 0; i < ArraySize(g_obs); i++) if(g_obs[i].bullish && price >= g_obs[i].low && price <= g_obs[i].high * 1.002) { nearOB = true; break; } bool liqAbove = false; for(int i = 0; i < ArraySize(g_liq); i++) if(g_liq[i].buySide && g_liq[i].price > price) { liqAbove = true; break; } return (inFVG || nearOB) && liqAbove; } //+------------------------------------------------------------------+ //| Check Bearish Entry | //+------------------------------------------------------------------+ bool CheckBearEntry(double price) { bool inFVG = false; for(int i = 0; i < ArraySize(g_fvgs); i++) if(!g_fvgs[i].bullish && price >= g_fvgs[i].lower && price <= g_fvgs[i].upper) { inFVG = true; break; } bool nearOB = false; for(int i = 0; i < ArraySize(g_obs); i++) if(!g_obs[i].bullish && price >= g_obs[i].low * 0.998 && price <= g_obs[i].high) { nearOB = true; break; } bool liqBelow = false; for(int i = 0; i < ArraySize(g_liq); i++) if(!g_liq[i].buySide && g_liq[i].price < price) { liqBelow = true; break; } return (inFVG || nearOB) && liqBelow; } //+------------------------------------------------------------------+ //| Get Directional Signal | //+------------------------------------------------------------------+ SignalDir GetDirectionalSignal(double &maxFlow, double &entry, double &sl, double &tp) { double curPrice = SymbolInfoDouble(g_symbol, SYMBOL_BID); LiquidityNode bullT, bearT; double bullFlow = 0.0, bearFlow = 0.0; if(FindNearestLiquidity(curPrice, true, bullT)) bullFlow = BuildDirectionalGraph(curPrice, bullT, true); if(FindNearestLiquidity(curPrice, false, bearT)) bearFlow = BuildDirectionalGraph(curPrice, bearT, false); bool bullSig = CheckBullEntry(curPrice); bool bearSig = CheckBearEntry(curPrice); if(bullSig && bullFlow >= bearFlow) { maxFlow = bullFlow; entry = SymbolInfoDouble(g_symbol, SYMBOL_ASK); sl = GetBullishSL(curPrice); tp = GetTP(entry, sl, true); return SIG_BUY; } if(bearSig && bearFlow > bullFlow) { maxFlow = bearFlow; entry = SymbolInfoDouble(g_symbol, SYMBOL_BID); sl = GetBearishSL(curPrice); tp = GetTP(entry, sl, false); return SIG_SELL; } maxFlow = MathMax(bullFlow, bearFlow); return SIG_NONE; } //+------------------------------------------------------------------+ //| MULTI-TIMEFRAME FLOW (LAYER 9) | //+------------------------------------------------------------------+ double ComputeMTFFlow() { return MTF_Weight_Fast * ComputeSingleTFFlow(TF_Fast) + MTF_Weight_Mid * ComputeSingleTFFlow(TF_Mid) + MTF_Weight_Slow * ComputeSingleTFFlow(TF_Slow) + MTF_Weight_Major * ComputeSingleTFFlow(TF_Major); } //+------------------------------------------------------------------+ //| Compute Single TimeFrame Flow | //+------------------------------------------------------------------+ double ComputeSingleTFFlow(ENUM_TIMEFRAMES tf) { double price = SymbolInfoDouble(g_symbol, SYMBOL_BID); int bars = iBars(g_symbol, tf); if(bars < 20) return 0.0; double bestBuyDist = DBL_MAX, bestSellDist = DBL_MAX; int limit = (int)MathMin(bars, LiquidityLookback); for(int i = 1; i < limit; i++) { double h = iHigh(g_symbol, tf, i); double l = iLow(g_symbol, tf, i); if(h > price && (h - price) < bestBuyDist) bestBuyDist = h - price; if(l < price && (price - l) < bestSellDist) bestSellDist = price - l; } if(bestBuyDist == DBL_MAX && bestSellDist == DBL_MAX) return 0.0; double avgVol = 0.0; int structs = 0; for(int i = 1; i < 20; i++) { avgVol += (double)iVolume(g_symbol, tf, i); double rng = iHigh(g_symbol,tf,i) - iLow(g_symbol,tf,i); if(rng > AverageCandleSize(i, 5) * 1.5) structs++; } avgVol /= 19.0; double volRatio = (avgVol > 0) ? (double)iVolume(g_symbol,tf,1) / avgVol : 1.0; double distBonus= (bestBuyDist < DBL_MAX) ? MathMin(50.0 / (bestBuyDist / g_pipValue), 5.0) : 0.0; return volRatio + structs * 0.5 + distBonus; }
This section defines the ICT-based entry conditions that determine whether a bullish or bearish setup is present. For a bullish setup, we check whether the current price is trading inside a bullish Fair Value Gap or near a bullish Order Block, while also confirming that buy-side liquidity exists above the market. The bearish logic follows the same approach in reverse by looking for bearish Fair Value Gaps, bearish Order Blocks, and available sell-side liquidity below the current price. These conditions ensure that trades are only considered when price is interacting with a valid market structure and has a clear liquidity target in the intended direction.
The directional signal logic then combines these ICT conditions with the Ford-Fulkerson liquidity-capacity analysis. Separate bullish and bearish graphs are built, and their maximum flow values are calculated to measure the available price-travel capacity toward the nearest liquidity pools. The side with the stronger valid flow is selected as the preferred trading direction, and the corresponding entry price, stop-loss, and take-profit levels are prepared. The multi-timeframe flow engine extends this analysis by calculating flow scores across several timeframes and combining them using weighted averages. This provides a broader view of market conditions, allowing higher timeframes to contribute more influence while still incorporating shorter-term liquidity and structure information into the final flow assessment.
//+------------------------------------------------------------------+ //| Stop-Loss / Take-Profit | //+------------------------------------------------------------------+ double GetBullishSL(double price) { double sl = price - 50.0 * g_pipValue; for(int i = 0; i < ArraySize(g_obs); i++) if(g_obs[i].bullish && g_obs[i].low < price) sl = MathMax(sl, g_obs[i].low - SL_BufferPips * g_pipValue); for(int i = 0; i < ArraySize(g_swingLows); i++) if(g_swingLows[i].price < price) sl = MathMax(sl, g_swingLows[i].price - SL_BufferPips * g_pipValue); return NormalizeDouble(sl, g_digits); } double GetBearishSL(double price) { double sl = price + 50.0 * g_pipValue; for(int i = 0; i < ArraySize(g_obs); i++) if(!g_obs[i].bullish && g_obs[i].high > price) sl = MathMin(sl, g_obs[i].high + SL_BufferPips * g_pipValue); for(int i = 0; i < ArraySize(g_swingHighs); i++) if(g_swingHighs[i].price > price) sl = MathMin(sl, g_swingHighs[i].price + SL_BufferPips * g_pipValue); return NormalizeDouble(sl, g_digits); } //+------------------------------------------------------------------+ //| Get Take-Profit | //+------------------------------------------------------------------+ double GetTP(double entry, double sl, bool bullish) { double risk = MathAbs(entry - sl); return bullish ? NormalizeDouble(entry + risk * TP_RR_Ratio, g_digits) : NormalizeDouble(entry - risk * TP_RR_Ratio, g_digits); } //+------------------------------------------------------------------+ //| DYNAMIC RISK SIZING (LAYER 7) | //+------------------------------------------------------------------+ double DynamicRisk(double flow) { double ratio = MathMin(flow / PeakFlowRef, 1.0); return BaseRiskPercent + ratio * (MaxRiskPercent - BaseRiskPercent); } //+------------------------------------------------------------------+ //| Calculate Lot Size | //+------------------------------------------------------------------+ double CalcLotSize(double riskPct, double slDist) { if(slDist <= 0) return SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_MIN); double balance = AccountInfoDouble(ACCOUNT_BALANCE); double riskAmt = balance * riskPct / 100.0; double tickVal = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_SIZE); double lotStep = SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_STEP); double minLot = SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_MAX); if(tickVal <= 0 || tickSize <= 0) return minLot; double slTicks = slDist / tickSize; double lots = riskAmt / (slTicks * tickVal); lots = MathFloor(lots / lotStep) * lotStep; return MathMax(minLot, MathMin(maxLot, lots)); } //+------------------------------------------------------------------+ //| UTILITIES | //+------------------------------------------------------------------+ int CountOpenTrades() { int count = 0; for(int i = PositionsTotal()-1; i >= 0; i--) if(PositionSelectByTicket(PositionGetTicket(i))) if(PositionGetString(POSITION_SYMBOL) == g_symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) count++; return count; } //+------------------------------------------------------------------+ //| DASHBOARD | //+------------------------------------------------------------------+ void ShowDashboard(SignalDir sig, double flow) { string dir = (sig == SIG_BUY) ? "BUY [LONG]" : (sig == SIG_SELL) ? "SELL [SHORT]" : "FLAT"; string qual = (flow > FlowThreshold) ? "QUALIFIED" : "FILTERED"; Comment( "\n══════════════════════════════════\n" " Ford-Fulkerson Liquidity EA\n" "══════════════════════════════════\n" " Symbol : ", g_symbol, "\n" " Signal : ", dir, "\n" " Max Flow : ", DoubleToString(flow, 2), "\n" " Threshold : ", DoubleToString(FlowThreshold, 2), "\n" " Status : ", qual, "\n" "──────────────────────────────────\n" " Swings H/L : ", ArraySize(g_swingHighs), " / ", ArraySize(g_swingLows), "\n" " FVGs : ", ArraySize(g_fvgs), "\n" " OrderBlocks: ", ArraySize(g_obs), "\n" " Liq Pools : ", ArraySize(g_liq), "\n" " Open Trades: ", CountOpenTrades(), "\n" "══════════════════════════════════" ); } //+------------------------------------------------------------------+
Lastly, this section manages trade protection levels by calculating stop loss and take profit prices from the detected market structures. For bullish trades, the stop loss is initially placed below the current price and then adjusted using nearby bullish Order Blocks and Swing Lows. The same process is applied in reverse for bearish trades, where nearby bearish Order Blocks and Swing Highs are used as protective levels. A configurable buffer is added to these structures to provide extra room for normal market fluctuations. Once the stop-loss has been established, the take-profit level is calculated automatically using the configured risk-to-reward ratio, ensuring that reward objectives remain proportional to the amount of risk taken on each trade.
Risk management is closely linked to the output of the Ford-Fulkerson liquidity-capacity engine. The DynamicRisk() function increases position risk as flow strength improves, allowing stronger liquidity-capacity paths to receive greater allocation while weaker paths receive less exposure. The lot size calculation then converts the selected risk percentage into an actual trading volume by considering account balance, stop-loss distance, tick value, and broker volume constraints. This approach ensures that position sizing remains consistent regardless of market conditions or instrument specifications.
The remaining utility functions support trade management and monitoring. The open-trade counter tracks active positions associated with the EA and prevents new trades from exceeding the configured limit. The dashboard provides a real-time summary of the EA's state directly on the chart, including the current signal, maximum flow value, qualification status, detected market structures, liquidity pool count, and the number of open positions. This gives us a clear view of how the Ford-Fulkerson engine is interpreting the market and whether current conditions are suitable for trading.
Backtest
The backtest was conducted across roughly a 2-month testing window from 01 April 2026 to 30 May 2026, with the following settings:

Below are the equity curve and the backtest results:


Conclusion
Through this implementation, a full multi-layer Expert Advisor was constructed from the ground up in MQL5. Market structure was detected automatically—Swing Highs, Swing Lows, Fair Value Gaps, Order Blocks, and Liquidity Pools were all identified and converted into graph nodes. A directed graph was then assembled for both bullish and bearish scenarios, with each edge receiving a capacity score derived from volume, reaction strength, distance, and structure quality. The Ford-Fulkerson algorithm, implemented using the Edmonds-Karp BFS variant, was then run across that graph to produce a maximum flow value. ICT entry conditions were wired in as the directional filter, dynamic position sizing was tied to the flow output, and a multi-timeframe flow aggregation layer was added on top. Every component has a defined role, and nothing executes without passing through the full qualification chain.
The trader leaves this article with something that is difficult to find—a logical bridge between discretionary ICT analysis and quantitative decision-making. They came in knowing how to read structure but struggling to know when that structure was strong enough to act on. They now have a framework that answers that question directly. Setups are no longer accepted or rejected on instinct alone. The network either has capacity or it does not, and that answer is computed, not guessed. Beyond the EA itself, the trader gains a new way to view market structure: a network of interconnected nodes through which liquidity must travel. That mental shift alone is worth more than any single trade signal the system will ever produce.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Price Action Analysis Toolkit Development (Part 72): Building a Gap Fill Indicator in MQL5
MQL5 Wizard Techniques you should know (Part 96): Using Wavelet Thresholding and LSTM Network in a Custom Money Management Class
Neural Networks in Trading: LSTM Optimization for Multivariate Time Series Forecasting (DA-CG-LSTM)
The Repository Pattern in MQL5: Abstracting Trade History Access for Testable EA Logic
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use