Automating Classic Market Methods in MQL5 (Part 1): Wyckoff Accumulation and Distribution - MQL5 Articles
MetaTrader 5 / Trading
Introduction
One of the most enduring frameworks in technical analysis is the Wyckoff method. Richard Wyckoff developed it in the 1930s after decades of observing how large institutional operators accumulate and distribute positions in financial markets.
The method describes market movement not as random noise, but as a series of cause-and-effect cycles driven by institutional supply and demand. These cycles leave identifiable structural footprints—the spring, the sign of strength, the upthrust, and the sign of weakness—that a prepared trader can detect and trade.
Although widely discussed, the Wyckoff method is rarely automated in MQL5. This is not because individual events are difficult to detect; for example, a close below support is trivial.
The challenge is that each Wyckoff event only carries meaning in the context of the events that preceded it. A spring is not merely a dip below support.
It is a dip below support that occurs within a defined range, following a prior downtrend, confirmed by a specific volume signature. Detecting it in isolation is meaningless.
Detecting it as part of a validated sequence is everything.
In this article, we build a complete Expert Advisor that automates this sequential detection using a finite state machine. The EA identifies accumulation structures on the H4 timeframe, detects the spring shakeout, confirms demand with a sign of strength, and enters long at the last point of support. For distribution, it mirrors the same logic in reverse: upthrust, a sign of weakness, and a short entry at the last point of supply. We will cover the following topics:
- Understanding the Wyckoff framework
- Why tick volume works as an institutional proxy
- The state machine architecture
- Implementation in MQL5
- Backtesting
- Conclusion
By the end, you will have a fully functional MQL5 Expert Advisor that detects and trades Wyckoff structures automatically.
Understanding the Wyckoff Framework
The Wyckoff method describes four recurring market cycles: accumulation, markup, distribution, and markdown. Accumulation occurs when institutions quietly build long positions in a sideways range following a downtrend.
Markup follows as price rises on institutional demand. Distribution occurs when institutions sell into an uptrend, creating a sideways range at the top.
Markdown follows as price falls on institutional supply.
This EA focuses on the two transition points—the end of Accumulation and the end of Distribution—because these offer the best-defined, highest-probability setups in the entire Wyckoff cycle.
The Accumulation Sequence
Accumulation moves through five phases. In Phase A, a selling climax on high volume stops the downtrend.
An automatic rally then defines the top of the range. A secondary test on lower volume confirms that selling pressure has dried up.
The second phase sees institutions quietly accumulate within the range as price oscillates between support and resistance. The third phase delivers the spring—a deliberate false breakdown below range support on lower-than-average volume, designed to flush out weak holders and collect liquidity before the markup begins.
The fourth phase confirms institutional demand with a sign of strength: a high-volume close above range resistance. The last point of support is the pullback that follows the sign of strength—the final low-risk entry before phase E begins and price leaves the range.
Fig. 1. Chart showing spring, sign of strength, and "LPS" entry.
The Distribution Mirror
Distribution reverses every element. The upthrust is the false breakout above range resistance on low volume—the institutional bull trap at the top.
The sign of weakness is the high-volume breakdown below range support. The last point of supply is the low-volume rally after the sign of weakness—the optimal short entry before "markdown" begins.
Fig. 2. Chart showing upthrust, sign of weakness, and "LPSY" entry.
What the EA Detects
The EA does not attempt to label every Wyckoff event from preliminary support to phase E. Full phase detection on live data requires subjective judgment that is difficult to codify reliably. Instead, it focuses on the events with the clearest, most objective definitions: range formation following a directional move, the spring or upthrust as the terminal shakeout, the sign of strength (SOS) or sign of weakness (SOW) as confirmation, and the last point of support (LPS) or the last point of supply (LPSY) as the entry trigger.
Why Tick Volume Works as an Institutional Proxy
Volume is central to every Wyckoff detection decision. This raises a practical question for MetaTrader 5 forex traders: real exchange volume is not available for currency pairs. MetaTrader 5 provides tick volume—the number of price changes per bar.
Tick volume is a valid proxy for trading activity. 85) with real traded volume on major forex pairs.
When institutions are active, prices change frequently. When the market is quiet, ticks are sparse.
The relative volume patterns that Wyckoff described—climactic volume on reversals, low volume on tests, and expanding volume on breakouts—manifest clearly in tick volume data.
The critical point is that we always work with relative volume, not absolute counts. A selling climax does not require a specific tick number.
It requires tick volume substantially above the average for that instrument on that timeframe. Every volume check in the EA computes a rolling average over the bars within the current range and compares each bar as a ratio against that baseline.
This approach is instrument-agnostic and works identically on EURUSD, gold, and any other symbols in MetaTrader 5.
The State Machine Architecture
Before writing the first line of code, the most important design decision must be made: the EA will use a finite state machine to track where the market is in the Wyckoff sequence. This is the only reliable architecture for implementing a sequential pattern detector.
Consider the naive alternative—an independent check in OnTick:
1if(iLow(_Symbol, PERIOD_CURRENT, 1) < supportLevel)
2 OpenLong(); // WRONG: fires on any dip, regardless of contextThis fires on any dip below any support level, with no regard for whether a range exists, whether a prior downtrend preceded it, or whether volume was appropriate. It is not a Wyckoff spring detector. It is a noise generator.
A state machine solves this by ensuring the EA is always in exactly one state and can only advance to the next state when the specific structural evidence for that state is present. The spring cannot be evaluated before the range is confirmed.
The sign of strength cannot be evaluated before the spring is confirmed. The entry cannot execute before the sign of strength is confirmed.
The sequence is enforced mechanically—it cannot be skipped.
The complete state flow for accumulation is as follows: "STATE_IDLE" → "STATE_RANGE_FORMING" → "STATE_SPRING_DETECTED" → "STATE_SOS_CONFIRMED" → "STATE_IN_TRADE." While the complete state flow for accumulation is as follows: "STATE_IDLE" → "STATE_RANGE_FORMING" → "STATE_UPTHRUST_DETECTED" → "STATE_SOW_CONFIRMED" → "STATE_IN_TRADE." Any state can reset to "STATE_IDLE" if the structure is invalidated—for example, if the price closes below the spring low after the spring is detected, the accumulation thesis is abandoned and the EA starts scanning for a fresh structure.
Implementation in MQL5
To create the program in MQL5, open MetaEditor, navigate to the Experts folder, click "New," and follow the prompts to create the file. We will build the EA piece by piece, explaining each section as we go.
Includes and Input Parameters
We begin by including the standard trade library and defining the EA's state machine enumeration and all input parameters.
1//+------------------------------------------------------------------+
2//| WyckoffEA.mq5 |
3//| Copyright 2026, Tola Moses Hector |
4//| https://t.me/tolahector |
5//+------------------------------------------------------------------+
6#property copyright "Copyright 2026, Tola Moses Hector"
7#property link "https://t.me/tolahector"
8#property version "1.00"
9#property description "Wyckoff Accumulation and Distribution EA"
10#property description "Detects Spring + SOS for long entries (LPS)"
11#property description "Detects Upthrust + SOW for short entries (LPSY)"
12#property description "H4 timeframe recommended"
13
14#include <Trade\Trade.mqh>
15
16//+------------------------------------------------------------------+
17//| EA State Machine |
18//+------------------------------------------------------------------+
19enum ENUM_WYCKOFF_STATE
20 {
21 STATE_IDLE, // No structure active
22 STATE_RANGE_FORMING, // Valid range identified — locked, watching for Spring/UT
23 STATE_SPRING_DETECTED, // Spring confirmed, watching for SOS
24 STATE_SOS_CONFIRMED, // SOS confirmed, watching for LPS entry
25 STATE_UPTHRUST_DETECTED, // Upthrust confirmed, watching for SOW
26 STATE_SOW_CONFIRMED, // SOW confirmed, watching for LPSY entry
27 STATE_IN_TRADE // Position open
28 };
29
30//+------------------------------------------------------------------+
31//| Input Parameters |
32//+------------------------------------------------------------------+
33input group "=== Range Detection ==="
34input int InpTrendBars = 15; // Bars of prior trend required
35input int InpMinRangeBars = 10; // Minimum bars to form range
36input int InpMaxRangeBars = 60; // Maximum bars to scan for range
37input double InpMinRangePips = 20.0; // Minimum range height (pips)
38input double InpMaxRangePips = 400.0; // Maximum range height (pips)
39input double InpSpringTolerance = 10.0; // Pips below support for Spring
40input int InpRangeWatchBars = 30; // Max bars to watch range before reset
41
42input group "=== Volume Settings ==="
43input double InpHighVolMult = 1.2; // High-volume multiplier (SOS/SOW)
44input double InpLowVolMult = 1.2; // Low-volume multiplier (Spring/Upthrust)
45
46input group "=== Entry and Risk ==="
47input double InpRiskPercent = 1.0; // Risk per trade (% of balance)
48input double InpRR = 2.0; // Risk-reward ratio
49input int InpATRPeriod = 14; // ATR period for stop distance
50input double InpATRMult = 1.5; // ATR multiplier for stop distance
51input int InpLPSBars = 8; // Bars to wait for LPS/LPSY pullback
52
53input group "=== General ==="
54input int InpMagicNumber = 777001; // Magic number
55input int InpSlippage = 10; // Slippage in points
56input bool InpShowLabels = true; // Draw event labels on chartmqh," which provides the "CTrade" class needed for order execution and position management. Furthermore, we define the "ENUM_WYCKOFF_STATE" enumeration with seven states that represent every possible position in the Wyckoff detection sequence.
This enumeration is the backbone of the entire EA—every detection decision flows from the current state. The input parameters are organized into four groups using the "input group" directive.
The "Range Detection" group controls how the EA identifies a valid trading range and prior trend. The "Volume Settings" group defines the relative volume thresholds used for every Wyckoff event.
The "Entry and Risk" group controls position sizing and stop placement. All parameters are configurable from the properties window without code changes.
Global Variables and Range Structure
Next, we define the data structure that stores all information about the current Wyckoff structure and the global variables that manage EA state.
1//+------------------------------------------------------------------+
2//| Wyckoff Range Data Structure |
3//+------------------------------------------------------------------+
4struct SWyckoffRange
5 {
6 double support; // Range support level
7 double resistance; // Range resistance level
8 int start_bar; // Bars back where range started
9 double avg_volume; // Average tick volume within range
10 bool bullish_bias; // true = accumulation, false = distribution
11 double spring_low; // Low of the Spring bar
12 double sos_high; // High of the SOS bar
13 double upthrust_high; // High of the Upthrust bar
14 double sow_low; // Low of the SOW bar
15 };
16
17//+------------------------------------------------------------------+
18//| Global Variables |
19//+------------------------------------------------------------------+
20ENUM_WYCKOFF_STATE g_state = STATE_IDLE; // Current EA state
21SWyckoffRange g_range; // Current range data
22CTrade g_trade; // Trade execution object
23int g_atr_handle = INVALID_HANDLE; // ATR indicator handle
24datetime g_last_bar = 0; // Last processed bar time
25int g_lps_count = 0; // Bars waited after SOS
26int g_lpsy_count = 0; // Bars waited after SOW
27int g_range_watch = 0; // Bars watched in RANGE_FORMINGThe "SWyckoffRange" struct serves as the EA's memory of the current structure. It stores the range boundaries, the volume baseline computed from within the range, the directional bias determined by whether a downtrend or uptrend preceded the range, and the price levels of each detected event.
Keeping all structure data in one place makes the code readable and eliminates scattered global variables. The global section instantiates "g_trade" from "CTrade" for all trade operations and creates "g_atr_handle" for the ATR indicator used in stop distance calculation.
Utility Functions
Before building the detection logic, we define several utility functions used throughout the EA.
1//+------------------------------------------------------------------+
2//| Returns pip size for the current symbol |
3//+------------------------------------------------------------------+
4double PipSize()
5 {
6 int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); // Get symbol digits
7 return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point; // Return pip size
8 }
9
10//+------------------------------------------------------------------+
11//| Calculates lot size from risk percent and stop distance |
12//+------------------------------------------------------------------+
13double CalcLots(double sl_pips)
14 {
15 double balance = AccountInfoDouble(ACCOUNT_BALANCE); // Get balance
16 double risk_money = balance * InpRiskPercent / 100.0; // Monetary risk
17 double tick_val = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); // Tick value
18 double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); // Tick size
19 double pip_size = PipSize(); // Get pip size
20 if(tick_size <= 0 || tick_val <= 0 || sl_pips <= 0)
21 return 0; // Validate inputs
22 double pip_value = (pip_size / tick_size) * tick_val; // Pip monetary value
23 double lots = risk_money / (sl_pips * pip_value); // Raw lot size
24 double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); // Volume step
25 double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); // Minimum lot
26 double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); // Maximum lot
27 lots = MathFloor(lots / step) * step; // Normalize to step
28 return MathMax(min_lot, MathMin(max_lot, lots)); // Clamp to limits
29 }
30
31//+------------------------------------------------------------------+
32//| Checks if EA has an open position on this symbol |
33//+------------------------------------------------------------------+
34bool PositionOpen()
35 {
36 for(int i = PositionsTotal() - 1; i >= 0; i--) // Iterate positions
37 {
38 ulong ticket = PositionGetTicket(i); // Get ticket
39 if(!PositionSelectByTicket(ticket))
40 continue; // Select position
41 if(PositionGetString(POSITION_SYMBOL) != _Symbol)
42 continue; // Check symbol
43 if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
44 continue; // Check magic
45 return true; // Position found
46 }
47 return false; // No position
48 }"PipSize()" detects whether the symbol has three or five decimal digits and returns the correct pip size in price units. This makes every subsequent distance calculation instrument-agnostic.
"CalcLots()" computes position size from a monetary risk target using "SYMBOL_TRADE_TICK_VALUE" and "SYMBOL_TRADE_TICK_SIZE"—the broker-provided values that convert price distance to account currency correctly for any instrument, including gold and indexes. This is more accurate than simple pip multiplication, which fails on nonstandard instruments.
"PositionOpen()" scans all open positions and returns true if any belong to this EA on the current symbol, using both the symbol name and magic number to avoid confusion with manual trades.
Chart Drawing Functions
Chart labels are important for verifying that the EA is detecting events correctly during testing. We define simple drawing helpers.
1//+------------------------------------------------------------------+
2//| Draws a horizontal line at the specified price level |
3//+------------------------------------------------------------------+
4void DrawHLine(string name, double price, color clr, ENUM_LINE_STYLE style)
5 {
6 if(!InpShowLabels)
7 return; // Check flag
8 string obj = "WYK_" + name; // Build object name
9 ObjectDelete(0, obj); // Remove existing
10 ObjectCreate(0, obj, OBJ_HLINE, 0, 0, price); // Create hline
11 ObjectSetInteger(0, obj, OBJPROP_COLOR, clr); // Set color
12 ObjectSetInteger(0, obj, OBJPROP_STYLE, style); // Set style
13 ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1); // Set width
14 ChartRedraw(0); // Redraw chart
15 }
16
17//+------------------------------------------------------------------+
18//| Places a text label at the specified time and price |
19//+------------------------------------------------------------------+
20void DrawLabel(string name, datetime time, double price, string text, color clr)
21 {
22 if(!InpShowLabels)
23 return; // Check flag
24 string obj = "WYK_" + name; // Build object name
25 ObjectDelete(0, obj); // Remove existing
26 ObjectCreate(0, obj, OBJ_TEXT, 0, time, price); // Create text object
27 ObjectSetString(0, obj, OBJPROP_TEXT, text); // Set text content
28 ObjectSetInteger(0, obj, OBJPROP_COLOR, clr); // Set color
29 ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9); // Set font size
30 ChartRedraw(0); // Redraw chart
31 }
32
33//+------------------------------------------------------------------+
34//| Removes all chart objects created by this EA |
35//+------------------------------------------------------------------+
36void ClearLabels()
37 {
38 int total = ObjectsTotal(0); // Get object count
39 for(int i = total - 1; i >= 0; i--) // Iterate backwards
40 {
41 string name = ObjectName(0, i); // Get object name
42 if(StringFind(name, "WYK_") == 0)
43 ObjectDelete(0, name); // Delete if EA's
44 }
45 ChartRedraw(0); // Redraw chart
46 }"DrawHLine()" places or updates a horizontal line at the range support and resistance levels. Prefixing all object names with "WYK_" allows "ClearLabels()" to reliably remove only this EA's objects without touching any manually placed lines or indicators. "DrawLabel()" places event name tags—"SPRING," "SOS," "UPTHRUST," "SOW," "LPS," and "LPSY"—directly on the chart at the price and time of each detected event.
Prior Trend Detection
A trading range only carries Wyckoff significance if it follows a directional move. We check this before accepting any range as valid.
1//+------------------------------------------------------------------+
2//| Returns true if a downtrend preceded the specified bar |
3//+------------------------------------------------------------------+
4bool HasPriorDowntrend(int from_bar)
5 {
6 double high_buf[], low_buf[]; // Price buffers
7 ArraySetAsSeries(high_buf, true); // Set as series
8 ArraySetAsSeries(low_buf, true); // Set as series
9 if(CopyHigh(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, high_buf) < InpTrendBars)
10 return false; // Copy highs
11 if(CopyLow(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, low_buf) < InpTrendBars)
12 return false; // Copy lows
13 double first_high = 0, second_low = DBL_MAX; // Init comparators
14 int half = InpTrendBars / 2; // Split midpoint
15 for(int i = 0; i < half; i++)
16 first_high = MathMax(first_high, high_buf[i + half]); // Find early high
17 for(int i = 0; i < half; i++)
18 second_low = MathMin(second_low, low_buf[i]); // Find recent low
19 return first_high > second_low + PipSize() * InpMinRangePips * 0.3; // Confirm descent
20 }
21
22//+------------------------------------------------------------------+
23//| Returns true if an uptrend preceded the specified bar |
24//+------------------------------------------------------------------+
25bool HasPriorUptrend(int from_bar)
26 {
27 double high_buf[], low_buf[]; // Price buffers
28 ArraySetAsSeries(high_buf, true); // Set as series
29 ArraySetAsSeries(low_buf, true); // Set as series
30 if(CopyHigh(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, high_buf) < InpTrendBars)
31 return false; // Copy highs
32 if(CopyLow(_Symbol, PERIOD_CURRENT, from_bar, InpTrendBars, low_buf) < InpTrendBars)
33 return false; // Copy lows
34 double first_low = DBL_MAX, second_high = 0; // Init comparators
35 int half = InpTrendBars / 2; // Split midpoint
36 for(int i = 0; i < half; i++)
37 first_low = MathMin(first_low, low_buf[i + half]); // Find early low
38 for(int i = 0; i < half; i++)
39 second_high = MathMax(second_high, high_buf[i]); // Find recent high
40 return second_high > first_low + PipSize() * InpMinRangePips * 0.3; // Confirm ascent
41 }Both functions split the "InpTrendBars" lookback period in half and compare the price structure of the earlier half against the recent half. For "HasPriorDowntrend()", the highest high should be in the earlier period and the lowest low in the recent period—confirming that the price moved downward over the lookback.
"HasPriorUptrend()" applies the mirror logic. The minimum distance threshold prevents a flat, trendless period from being misclassified as a trend.
Range Detection
With prior trend detection in place, we can now identify valid trading ranges. The function scans from the minimum required bar count upward, returning the first range that satisfies all conditions rather than always expanding to the maximum. This prevents the state machine from being anchored to an overly wide, unfocused range.
When both a prior downtrend and a prior uptrend are detected before the same range, the EA resolves the ambiguity by comparing the first and last close of the trend period immediately before the range. If the price fell into the range, the preceding move was a downtrend, and the bias is accumulation. If the price rose into the range, the preceding move was an uptrend, and the bias is distribution.
1//+------------------------------------------------------------------+
2//| Scans recent bars once to identify a valid Wyckoff range |
3//+------------------------------------------------------------------+
4bool DetectRange()
5 {
6 double high_buf[], low_buf[]; // Price buffers
7 long vol_buf[]; // Volume buffer
8 ArraySetAsSeries(high_buf, true); // Set as series
9 ArraySetAsSeries(low_buf, true); // Set as series
10 ArraySetAsSeries(vol_buf, true); // Set as series
11 int bars = InpMaxRangeBars + InpTrendBars + 5; // Total bars needed
12 if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, bars, high_buf) < bars)
13 return false; // Copy highs
14 if(CopyLow(_Symbol, PERIOD_CURRENT, 1, bars, low_buf) < bars)
15 return false; // Copy lows
16 if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 1, bars, vol_buf) < bars)
17 return false; // Copy volumes
18 double pip = PipSize(); // Get pip size
19 double min_h = InpMinRangePips * pip; // Min height in price
20 double max_h = InpMaxRangePips * pip; // Max height in price
21//--- Try each possible range length from minimum to maximum
22 for(int range_bars = InpMinRangeBars; range_bars <= InpMaxRangeBars; range_bars++) // Try lengths
23 {
24 double rh = 0, rl = DBL_MAX; // Init bounds
25 for(int i = 0; i < range_bars; i++) // Scan range bars
26 {
27 rh = MathMax(rh, high_buf[i]); // Update range high
28 rl = MathMin(rl, low_buf[i]); // Update range low
29 }
30 double height = rh - rl; // Compute height
31 if(height < min_h)
32 continue; // Too narrow, try longer
33 if(height > max_h)
34 break; // Too wide, stop
35 //--- Check that all bars stay within 25% tolerance of range height
36 double tol = height * 0.25; // Tolerance band
37 bool fits = true; // Fit flag
38 for(int i = 0; i < range_bars; i++) // Check each bar
39 {
40 if(high_buf[i] > rh + tol || low_buf[i] < rl - tol) // Bar outside range
41 {
42 fits = false; // Mark as not fitting
43 break; // Stop checking
44 }
45 }
46 if(!fits)
47 continue; // Skip this length
48 //--- Confirm a prior trend before the range
49 bool down_before = HasPriorDowntrend(range_bars + 1); // Check downtrend
50 bool up_before = HasPriorUptrend(range_bars + 1); // Check uptrend
51 if(!down_before && !up_before)
52 continue; // No prior trend
53 //--- Compute average volume within the range
54 double avg_vol = 0; // Volume sum
55 for(int i = 0; i < range_bars; i++)
56 avg_vol += (double)vol_buf[i]; // Accumulate
57 avg_vol /= range_bars; // Compute average
58 //--- Populate the range struct
59 g_range.support = rl; // Store support
60 g_range.resistance = rh; // Store resistance
61 g_range.start_bar = range_bars; // Store bar count
62 g_range.avg_volume = avg_vol; // Store average vol
63 //--- When both trends detected, pick the more dominant one
64 if(down_before && up_before)
65 {
66 //--- Measure which directional move was larger immediately before range
67 double high_buf[], low_buf[];
68 ArraySetAsSeries(high_buf, true);
69 ArraySetAsSeries(low_buf, true);
70 int trend_bars = InpTrendBars;
71 CopyHigh(_Symbol, PERIOD_CURRENT, range_bars + 1, trend_bars, high_buf);
72 CopyLow(_Symbol, PERIOD_CURRENT, range_bars + 1, trend_bars, low_buf);
73 double trend_high = 0, trend_low = DBL_MAX;
74 for(int k = 0; k < trend_bars; k++)
75 {
76 trend_high = MathMax(trend_high, high_buf[k]);
77 trend_low = MathMin(trend_low, low_buf[k]);
78 }
79 double first_close = iClose(_Symbol, PERIOD_CURRENT, range_bars + trend_bars);
80 double last_close = iClose(_Symbol, PERIOD_CURRENT, range_bars + 1);
81 //--- If price fell into the range = downtrend before = accumulation
82 //--- If price rose into the range = uptrend before = distribution
83 g_range.bullish_bias = (last_close < first_close);
84 }
85 else
86 g_range.bullish_bias = down_before; // Set Bias
87 g_range.spring_low = 0; // Clear spring low
88 g_range.sos_high = 0; // Clear SOS high
89 g_range.upthrust_high = 0; // Clear upthrust
90 g_range.sow_low = 0; // Clear SOW low
91 DrawHLine("SUPPORT", g_range.support, clrGreen, STYLE_DASH); // Draw support
92 DrawHLine("RESISTANCE", g_range.resistance, clrRed, STYLE_DASH); // Draw resistance
93 Print(StringFormat("WyckoffEA: Range locked | Bias: %s | S: %.5f | R: %.5f | Bars: %d | AvgVol: %.0f",
94 g_range.bullish_bias ? "ACCUMULATION" : "DISTRIBUTION",
95 g_range.support, g_range.resistance, range_bars, avg_vol)); // Log result
96 return true; // Range valid
97 }
98 return false; // No range found
99 }"DetectRange()" seeds each candidate range from the minimum bar count and expands upward. The 25% tolerance band accommodates the natural variability of real Wyckoff ranges without accepting wildly expanded zones.
bullish_bias" accordingly. The average volume is then computed from the range bars—this becomes the baseline for every subsequent volume comparison.
Note that we copy from bar 1, not bar 0, to avoid acting on incomplete bar data.
Spring and Upthrust Detection
With a confirmed range and its volume baseline, we watch for the terminal shakeout event that signals the end of phase B.
1//+------------------------------------------------------------------+
2//| Checks the last closed bar for a valid Spring |
3//+------------------------------------------------------------------+
4bool CheckSpring()
5 {
6 double low1 = iLow(_Symbol, PERIOD_CURRENT, 1); // Last bar low
7 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); // Last bar close
8 double pip = PipSize(); // Get pip size
9 double thresh = g_range.support - InpSpringTolerance * pip; // Spring threshold
10 if(low1 < thresh && close1 > g_range.support) // Penetrate and recover
11 {
12 long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); // Last bar volume
13 if((double)vol1 < g_range.avg_volume * InpLowVolMult) // Below volume threshold
14 {
15 g_range.spring_low = low1; // Store Spring low
16 datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time
17 DrawLabel("SPRING", t1, low1 - pip * 5, "SPRING", clrLime); // Draw label
18 Print(StringFormat("WyckoffEA: SPRING | Low: %.5f | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
19 low1, close1, vol1, g_range.avg_volume)); // Log event
20 return true; // Spring confirmed
21 }
22 }
23 return false; // Not a Spring
24 }
25
26//+------------------------------------------------------------------+
27//| Checks the last closed bar for a valid Upthrust |
28//+------------------------------------------------------------------+
29bool CheckUpthrust()
30 {
31 double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1); // Last bar high
32 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); // Last bar close
33 double pip = PipSize(); // Get pip size
34 double thresh = g_range.resistance + InpSpringTolerance * pip; // Upthrust threshold
35 if(high1 > thresh && close1 < g_range.resistance) // Penetrate and reverse
36 {
37 long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); // Last bar volume
38 if((double)vol1 < g_range.avg_volume * InpLowVolMult) // Below volume threshold
39 {
40 g_range.upthrust_high = high1; // Store Upthrust high
41 datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time
42 DrawLabel("UT", t1, high1 + pip * 5, "UPTHRUST", clrOrangeRed); // Draw label
43 Print(StringFormat("WyckoffEA: UPTHRUST | High: %.5f | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
44 high1, close1, vol1, g_range.avg_volume)); // Log event
45 return true; // Upthrust confirmed
46 }
47 }
48 return false; // Not an Upthrust
49 }"CheckSpring()" evaluates three conditions simultaneously on the last closed bar. The low must penetrate below the spring threshold—the support level minus the configured tolerance.
The close must recover back above support within the same bar. The tick volume must be below the low-volume multiplier times the range average—confirming absorption rather than aggressive selling.
All three must be true for the spring to be valid. spring_low" provides an invalidation level: if a subsequent bar closes below this level, the accumulation thesis is abandoned.
"CheckUpthrust()" applies the identical logic in reverse at range resistance.
Sign of Strength and Sign of Weakness
The spring and upthrust are necessary but not sufficient. We need confirmation that institutional demand or supply has actually taken control.
1//+------------------------------------------------------------------+
2//| Checks the last closed bar for a Sign of Strength |
3//+------------------------------------------------------------------+
4bool CheckSOS()
5 {
6 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); // Last bar close
7 double high1 = iHigh(_Symbol, PERIOD_CURRENT, 1); // Last bar high
8 if(close1 > g_range.resistance) // Close above resistance
9 {
10 long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); // Last bar volume
11 if((double)vol1 > g_range.avg_volume * InpHighVolMult) // Above volume threshold
12 {
13 g_range.sos_high = high1; // Store SOS high
14 datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time
15 DrawLabel("SOS", t1, high1 + PipSize() * 5, "SOS", clrDodgerBlue); // Draw label
16 Print(StringFormat("WyckoffEA: SOS | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
17 close1, vol1, g_range.avg_volume)); // Log event
18 return true; // SOS confirmed
19 }
20 }
21 return false; // Not a SOS
22 }
23
24//+------------------------------------------------------------------+
25//| Checks the last closed bar for a Sign of Weakness |
26//+------------------------------------------------------------------+
27bool CheckSOW()
28 {
29 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); // Last bar close
30 double low1 = iLow(_Symbol, PERIOD_CURRENT, 1); // Last bar low
31 if(close1 < g_range.support) // Close below support
32 {
33 long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); // Last bar volume
34 if((double)vol1 > g_range.avg_volume * InpHighVolMult) // Above volume threshold
35 {
36 g_range.sow_low = low1; // Store SOW low
37 datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time
38 DrawLabel("SOW", t1, low1 - PipSize() * 5, "SOW", clrCrimson); // Draw label
39 Print(StringFormat("WyckoffEA: SOW | Close: %.5f | Vol: %I64d | AvgVol: %.0f",
40 close1, vol1, g_range.avg_volume)); // Log event
41 return true; // SOW confirmed
42 }
43 }
44 return false; // Not a SOW
45 }The volume logic for the sign of strength and sign of weakness is the direct opposite of the spring and upthrust. Where the spring required low volume to confirm absorption, the sign of strength requires high volume to confirm institutional demand breaking price out of the range.
A breakout above resistance on weak volume is unconvincing—institutions are not driving it. 2 times the range average carries structural conviction.
"CheckSOW()" applies the identical requirement for the distribution breakdown.
Entry Logic: LPS and LPSY
After a sign of strength or a sign of weakness confirmation, the EA waits for a pullback before entering. Chasing a breakout bar produces poor risk-reward. The last point of support and the last point of supply—the pullbacks after the sign of strength and sign of weakness—provide the optimal entries.
1//+------------------------------------------------------------------+
2//| Waits for LPS pullback and opens long position |
3//+------------------------------------------------------------------+
4void CheckLPSEntry()
5 {
6 g_lps_count++; // Increment wait counter
7 if(g_lps_count > InpLPSBars) // Wait window expired
8 {
9 Print("WyckoffEA: LPS window expired — resetting."); // Log expiry
10 g_state = STATE_IDLE; // Reset state
11 ClearLabels(); // Clear chart
12 return; // Exit function
13 }
14 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); // Last bar close
15 long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); // Last bar volume
16 bool pulled_back = (close1 < g_range.sos_high && close1 > g_range.support); // Pullback check
17 bool low_vol = ((double)vol1 < g_range.avg_volume * InpHighVolMult); // Volume check
18 Print(StringFormat("WyckoffEA: LPS check %d/%d | Close: %.5f | PulledBack: %s | LowVol: %s",
19 g_lps_count, InpLPSBars, close1,
20 pulled_back ? "YES" : "NO", low_vol ? "YES" : "NO")); // Log check
21 if(pulled_back && low_vol) // LPS conditions met
22 {
23 double atr_buf[]; // ATR buffer
24 ArraySetAsSeries(atr_buf, true); // Set as series
25 if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
26 return; // Copy ATR value
27 double atr = atr_buf[0]; // ATR value
28 double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); // Current ask
29 double sl = ask - atr * InpATRMult; // Stop loss price
30 double sl_pip = (ask - sl) / PipSize(); // Stop in pips
31 double tp = ask + sl_pip * InpRR * PipSize(); // Take profit price
32 double lots = CalcLots(sl_pip); // Calculate lot size
33 if(lots <= 0)
34 return; // Invalid lot size
35 long stop_lv = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); // Broker stop level
36 double min_dist = stop_lv * _Point; // Minimum distance
37 if(ask - sl < min_dist)
38 sl = ask - min_dist - _Point; // Adjust SL if needed
39 if(tp - ask < min_dist)
40 tp = ask + min_dist + _Point; // Adjust TP if needed
41 if(g_trade.Buy(lots, _Symbol, ask, sl, tp, "Wyckoff LPS Long")) // Open long
42 {
43 datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time
44 DrawLabel("LPS", t1, iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 3, "LPS", clrGold); // Draw label
45 Print(StringFormat("WyckoffEA: LONG opened | Lots: %.2f | Ask: %.5f | SL: %.5f | TP: %.5f",
46 lots, ask, sl, tp)); // Log trade
47 g_state = STATE_IN_TRADE; // Update state
48 }
49 }
50 }
51
52//+------------------------------------------------------------------+
53//| Waits for LPSY rally and opens short position |
54//+------------------------------------------------------------------+
55void CheckLPSYEntry()
56 {
57 g_lpsy_count++; // Increment wait counter
58 if(g_lpsy_count > InpLPSBars) // Wait window expired
59 {
60 Print("WyckoffEA: LPSY window expired — resetting."); // Log expiry
61 g_state = STATE_IDLE; // Reset state
62 ClearLabels(); // Clear chart
63 return; // Exit function
64 }
65 double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); // Last bar close
66 long vol1 = iTickVolume(_Symbol, PERIOD_CURRENT, 1); // Last bar volume
67 bool rallied = (close1 > g_range.sow_low && close1 < g_range.resistance); // Rally check
68 bool low_vol = ((double)vol1 < g_range.avg_volume * InpHighVolMult); // Volume check
69 Print(StringFormat("WyckoffEA: LPSY check %d/%d | Close: %.5f | Rallied: %s | LowVol: %s",
70 g_lpsy_count, InpLPSBars, close1,
71 rallied ? "YES" : "NO", low_vol ? "YES" : "NO")); // Log check
72 if(rallied && low_vol) // LPSY conditions met
73 {
74 double atr_buf[]; // ATR buffer
75 ArraySetAsSeries(atr_buf, true); // Set as series
76 if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
77 return; // Copy ATR value
78 double atr = atr_buf[0]; // ATR value
79 double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); // Current bid
80 double sl = bid + atr * InpATRMult; // Stop loss price
81 double sl_pip = (sl - bid) / PipSize(); // Stop in pips
82 double tp = bid - sl_pip * InpRR * PipSize(); // Take profit price
83 double lots = CalcLots(sl_pip); // Calculate lot size
84 if(lots <= 0)
85 return; // Invalid lot size
86 long stop_lv = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); // Broker stop level
87 double min_dist = stop_lv * _Point; // Minimum distance
88 if(sl - bid < min_dist)
89 sl = bid + min_dist + _Point; // Adjust SL if needed
90 if(bid - tp < min_dist)
91 tp = bid - min_dist - _Point; // Adjust TP if needed
92 if(g_trade.Sell(lots, _Symbol, bid, sl, tp, "Wyckoff LPSY Short")) // Open short
93 {
94 datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1); // Get bar time
95 DrawLabel("LPSY", t1, iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 3, "LPSY", clrGold); // Draw label
96 Print(StringFormat("WyckoffEA: SHORT opened | Lots: %.2f | Bid: %.5f | SL: %.5f | TP: %.5f",
97 lots, bid, sl, tp)); // Log trade
98 g_state = STATE_IN_TRADE; // Update state
99 }
100 }
101 }Both entry functions increment a bar counter each time they are called. If the pullback does not materialize within "InpLPSBars" bars, the state machine resets to "STATE_IDLE," and the structure is abandoned—the EA never chases a trade.
5 ATR below the entry for longs and above for shorts, making it proportional to current market volatility rather than a fixed pip distance. The take profit is set at the configured risk-reward ratio.
The broker's "SYMBOL_TRADE_STOPS_LEVEL" is checked before execution, and the stop and take profit are adjusted if they fall within the broker's minimum distance requirement.
OnInit, OnDeinit, and OnTick
With all detection and entry functions defined, the event handlers bring the EA to life.
1//+------------------------------------------------------------------+
2//| Expert initialization function |
3//+------------------------------------------------------------------+
4int OnInit()
5 {
6 g_atr_handle = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod); // Create ATR handle
7 if(g_atr_handle == INVALID_HANDLE) // Check handle
8 {
9 Print("WyckoffEA: ATR indicator handle creation failed."); // Log error
10 return INIT_FAILED; // Return failure
11 }
12 g_trade.SetExpertMagicNumber(InpMagicNumber); // Set magic number
13 g_trade.SetDeviationInPoints(InpSlippage); // Set slippage
14 g_state = STATE_IDLE; // Initialize state
15 g_lps_count = 0; // Initialize LPS counter
16 g_lpsy_count = 0; // Initialize LPSY counter
17 g_range_watch = 0; // Initialize watch counter
18 g_last_bar = 0; // Initialize bar time
19 Print("WyckoffEA initialized | Symbol: ", _Symbol,
20 " | TF: ", EnumToString(Period()),
21 " | Magic: ", InpMagicNumber); // Log initialization
22 return INIT_SUCCEEDED; // Return success
23 }
24
25//+------------------------------------------------------------------+
26//| Expert deinitialization function |
27//+------------------------------------------------------------------+
28void OnDeinit(const int reason)
29 {
30 IndicatorRelease(g_atr_handle); // Release ATR handle
31 ClearLabels(); // Remove chart objects
32 }
33
34//+------------------------------------------------------------------+
35//| Expert tick function |
36//+------------------------------------------------------------------+
37void OnTick()
38 {
39 datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0); // Current bar open time
40 if(current_bar == g_last_bar)
41 return; // Skip if same bar
42 g_last_bar = current_bar; // Update last bar time
43 if(g_state == STATE_IN_TRADE) // In trade state
44 {
45 if(!PositionOpen()) // Position has closed
46 {
47 Print("WyckoffEA: Trade closed — returning to IDLE."); // Log closure
48 g_state = STATE_IDLE; // Reset state
49 g_lps_count = 0; // Reset LPS counter
50 g_lpsy_count = 0; // Reset LPSY counter
51 g_range_watch = 0; // Reset watch counter
52 ClearLabels(); // Clear chart
53 }
54 return; // Exit tick
55 }
56 switch(g_state) // State machine switch
57 {
58 case STATE_IDLE: // Idle state
59 if(DetectRange()) // Range found
60 {
61 g_state = STATE_RANGE_FORMING; // Advance state
62 g_range_watch = 0; // Reset watch counter
63 }
64 break; // End case
65 case STATE_RANGE_FORMING: // Range locked — watch only
66 g_range_watch++; // Increment watch counter
67 if(g_range_watch > InpRangeWatchBars) // Range stale
68 {
69 Print("WyckoffEA: Range watch expired — returning to IDLE."); // Log expiry
70 g_state = STATE_IDLE; // Reset state
71 ClearLabels(); // Clear chart
72 break; // End case
73 }
74 if(g_range.bullish_bias) // Accumulation bias
75 {
76 if(CheckSpring())
77 g_state = STATE_SPRING_DETECTED; // Spring found
78 }
79 else // Distribution bias
80 {
81 if(CheckUpthrust())
82 g_state = STATE_UPTHRUST_DETECTED; // Upthrust found
83 }
84 break; // End case
85 case STATE_SPRING_DETECTED: // Spring detected
86 if(CheckSOS()) // SOS found
87 {
88 g_state = STATE_SOS_CONFIRMED; // Advance state
89 g_lps_count = 0; // Reset LPS counter
90 }
91 else
92 if(iClose(_Symbol, PERIOD_CURRENT, 1) < g_range.spring_low) // Spring failed
93 {
94 Print("WyckoffEA: Spring invalidated — returning to IDLE."); // Log failure
95 g_state = STATE_IDLE; // Reset state
96 ClearLabels(); // Clear chart
97 }
98 break; // End case
99 case STATE_SOS_CONFIRMED: // SOS confirmed
100 CheckLPSEntry(); // Check LPS entry
101 break; // End case
102 case STATE_UPTHRUST_DETECTED: // Upthrust detected
103 if(CheckSOW()) // SOW found
104 {
105 g_state = STATE_SOW_CONFIRMED; // Advance state
106 g_lpsy_count = 0; // Reset LPSY counter
107 }
108 else
109 if(iClose(_Symbol, PERIOD_CURRENT, 1) > g_range.upthrust_high) // Upthrust failed
110 {
111 Print("WyckoffEA: Upthrust invalidated — returning to IDLE."); // Log failure
112 g_state = STATE_IDLE; // Reset state
113 ClearLabels(); // Clear chart
114 }
115 break; // End case
116 case STATE_SOW_CONFIRMED: // SOW confirmed
117 CheckLPSYEntry(); // Check LPSY entry
118 break; // End case
119 }
120 }"OnInit()" creates the ATR indicator handle and returns "INIT_FAILED" immediately if creation fails, preventing the EA from running without its stop distance calculator. "OnDeinit()" releases the indicator handle and cleans up all chart objects.
"OnTick()" uses a new-bar gate—comparing the current bar's open time against the stored "g_last_bar"—so all detection logic runs exactly once per closed bar rather than on every tick. The switch statement makes the state machine readable at a glance: each case has one responsibility, each transition has one trigger, and the code cannot reach any detection function without passing through all preceding states.
Backtesting
To test the EA, open the MetaTrader 5 Strategy Tester and use these settings: Symbol = EURUSD; Timeframe = H4; Modeling = Every tick based on real ticks; Initial deposit = $10,000; Period = 2022.01.01–2024.12.31. Use the following inputs: "InpTrendBars" = 15; "InpMinRangeBars" = 15; "InpMaxRangeBars" = 60; "InpMinRangePips" = 20; "InpMaxRangePips" = 400; "InpSpringTolerance" = 10; "InpHighVolMult" = 1.2; "InpLowVolMult" = 1.2; "InpRiskPercent" = 1.0; "InpRR" = 2.0; "InpATRPeriod" = 14; "InpATRMult" = 1.5; and "InpLPSBars" = 8.
What to Expect
Wyckoff structures are not common. On EURUSD H4, expect between four and ten qualifying setups per year.
This is correct behavior—the EA does not generate signals constantly. It waits for the full sequential evidence to accumulate before committing.
When it does enter, the structural backing is complete: confirmed range, confirmed spring or upthrust, confirmed sign of strength or sign of weakness, and a pullback entry.
Trade duration will be bimodal: many short trades stopped at the initial stop during failed structures, and fewer but significantly longer winners that capture the markup or markdown phase after a complete accumulation or distribution. The average winner should be substantially larger than the average loser. If the profit factor is consistently below 1.0, increase "InpHighVolMult" to require a stronger sign of strength and a sign of weakness confirmation.
Test Results
Demonstration on EURUSD H4.
Fig. 3. Visual demonstration of detection and entry.
Fig. 4. Test results: equity and balance curve.
Fig. 5. Test results.
Fig. 6. Test results, entries.
Known Limitations
Wyckoff structures require prior trend context. In genuinely sideways, trendless markets, the range detector may identify false structures. Increase "InpMinRangePips" and "InpTrendBars" if too many low-quality ranges are detected on a particular instrument.
Tick volume is a proxy, not real volume. The relative volume patterns are reliable.
Absolute threshold values may need adjustment per broker. Run initial tests and observe the Journal tab for the volume diagnostics printed by each detection function.
The EA tracks one structure at a time. If a spring is invalidated because the price closes below the spring low, the state machine resets to "STATE_IDLE" and begins scanning for a new range. This is correct Wyckoff behavior—a failed spring changes the structural interpretation.
The "LPS" and "LPSY" entry window is fixed. After a sign of strength or a sign of weakness confirmation, the EA waits up to "InpLPSBars" bars for the pullback.
In strong markup or markdown moves, price may not pull back within this window. The EA resets rather than chasing the move.
This is conservative behavior by design.
The code is a demonstration of the Wyckoff concept in MQL5. Before live deployment, adjust volume multipliers to your symbol's typical tick volume behavior and run a minimum 24-month backtest on a demo account.
Conclusion
Wyckoff spent decades studying the same market behavior that modern traders call "Smart Money Concepts," "institutional footprints," and "liquidity engineering." His framework remains relevant because it describes market structure in terms of cause and effect—not patterns and indicators—and cause and effect does not go out of date.
The challenge of automating Wyckoff is not detecting individual events. Any EA can detect a close below support.
The challenge is ensuring that each event only means something in the context of the events that preceded it. That context is what the state machine in this article enforces.
A spring is not just a false breakdown. It is a false breakdown that occurs within a defined range, after a prior downtrend, on lower-than-average volume.
Remove any of those conditions, and the signal is not a Wyckoff spring—it is just a dip. The state machine makes this impossible to bypass.
Every function introduced in this article has a single job. Furthermore, every state transition has a single trigger.
Every volume check uses a relative ratio computed from the current structure, not a hard-coded number. The result is an EA that adapts to different instruments and volatility regimes while remaining anchored to the structural logic Wyckoff described.
The EA does not trade often. When it does, the full weight of a confirmed Wyckoff sequence is behind the entry.
All code was compiled and tested in MetaTrader 5. mq5 source file is attached to this article.
mq5 to MQL5\\Experts\\ and compile in MetaEditor with no additional dependencies. Recommended for EURUSD on H4.
Always test on a demo account before live deployment.
Attached files |
WyckoffEA.mq5(41.36 KB)
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.
-
Advanced Trading Systems
I started as a trader, analyzing charts, price action, and market structure long before writing my first line of MQL code. That trading foundation shapes every Expert Advisor and indicator I build.
Today, I develop professional-grade Expert Advisors and indicators for MetaTrader, with a focus on robustness, clarity, and real-world usability. My work emphasizes clean architecture, risk-aware logic, and long-term maintainability rather than over-optimized or curve-fit systems.
Beyond trading software, I also develop full-stack solutions, including front-end and back-end web applications, API integrations, and server deployments. I work with cloud infrastructure such as Microsoft Azure to support scalable, secure, and high-performance trading and data systems.
My goal is simple: built well-engineered, professional tools that traders can understand, trust, and confidently use in live market environments.
Other articles by this author
We bridge Python-native artifacts to MQL5 for tick-accurate CPCV backtesting. The export script converts the ONNX model, calibrator, feature spec, and path masks to flat files, while the expert advisor rebuilds features, performs ONNX inference with calibration, and trades on real ticks. The Strategy Tester runs each combinatorial path, and Python aggregates per-path equities into a path Sharpe distribution to assess robustness after spread, slippage, and commission.
Seasonality Indicator by Hours, Days of the Week, and Days of the Month
The article explains how to develop a tool for analyzing recurring price patterns in financial markets — by day of the month (1-31), day of the week (Monday-Sunday), or hour of the day (0-23). The indicator analyzes historical data, calculates the average return for each period, and displays the results as a histogram with a forecast. It includes customizable parameters: seasonality type, number of bars analyzed, display as percentages or absolute values, chart colors.
We replace native MetaTrader chart objects with a canvas-based drawing engine that renders tools pixel-by-pixel on a full-chart bitmap layer. The article implements persistent object storage with per-tool style memory, precise hit testing, selection, whole-object dragging, and handle manipulation. It also adds new line tools, a reorganized category system with a one-click delete action, and a rubber-band preview for multi-click placement.
Backtracking Search Algorithm (BSA)
What if an optimization algorithm could remember its past journeys and use that memory to find better solutions? BSA does just that – balancing exploration with revisiting the tried and true.
In this article, we reveal the secrets of the algorithm. A simple idea, minimum parameters and a stable result.
[
\
Fully featured platform for any devices and web browsers\
\
This website uses cookies. Learn more about our Cookies Policy.
You are missing trading opportunities:
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
RegistrationLog in
latin characters without spaces
a password will be sent to this email
An error occurred
You agree to website policy and terms of use
If you do not have an account, please register
Allow the use of cookies to log in to the MQL5.com website.
Please enable the necessary setting in your browser, otherwise you will not be able to log in.


