Making Custom Indicators for Beginners (Part 2): Fisher-style Indicator
Introduction
The Fisher Transform is a technique originally developed in statistics by Ronald Fisher, used to convert a bounded, non-normal set of values into one that makes extremes more pronounced and the distribution more symmetrical. Applied to financial price data, this same transformation reshapes price movement so that its turning points become sharp and clearly defined, rather than the smoothed, gradual reversals typical of most conventional oscillators.
At its core, the calculation happens in two stages. Price is first normalized into a fixed range based on its recent highs and lows, similar in spirit to a stochastic-style calculation. That normalized value is then passed through the Fisher Transform formula itself — a logarithmic function that stretches values near the edges of the range, making extremes and reversals stand out far more distinctly than they would in the raw, untransformed price.
Building on the concepts and techniques introduced in the first part of this series, this article builds a Fisher-style Indicator in MQL5 from the ground up. It explains the math behind each step in plain terms, then implements the indicator piece by piece. Finally, it presents a companion Expert Advisor based on the indicator's signals and tests it across multiple symbols.In practice, using Fisher Transform Style Indicator as an "extreme detector" comes with three specific challenges:
- Recursive dependence on previous bars — the formula is easy to calculate once on paper, but implementing it correctly in MQL5 is harder, since each value depends on the state carried over from the previous bar.
- Logarithm instability near ±1 — without proper normalization and clamping, the logarithm step periodically produces invalid values and breaks the indicator's buffers.
- Turning the line into a usable signal — even after obtaining a clean oscillator line, it isn't immediately obvious how to convert it into a reproducible signal an EA can act on, without re-painting and without mixing the indicator's own calculations into the trading logic.
This section builds a Fisher-style indicator from scratch that addresses all three: it works reliably across symbols, outputs a single clear value to its buffer, and can be read directly by an Expert Advisor based on closed bars only.
How to Use It
Reading this indicator is a matter of watching two things together: the level the Fisher line reaches, and what it does after reaching that level.
The default InpLookbackPeriod of 10 keeps the oscillator responsive to fairly recent price action. In practice, a reading beyond ±1.5 already represents a meaningful extreme relative to the recent range, though some traders prefer a stricter threshold — using ±2 instead of ±1.5, for example — to filter out smaller, less reliable swings and wait only for the sharpest, most extended moves. There's no single correct threshold; it's a direct trade-off between how many signals appear and how strong each one tends to be.
The signal itself is not the moment the line crosses a threshold — it's the moment the line reaches beyond that threshold and then turns back toward zero. A Fisher value climbing steadily past +2 is still in an active move; the actionable point is the peak, where the line stops climbing and starts descending again. That turn is what marks a potential sell opportunity. The same logic applies in reverse at the lower threshold: a trough below -2 that turns back upward marks a potential buy opportunity.
This two-part rule (reach an extreme, then confirm a reversal) is stricter than a simple threshold crossing. The companion Expert Advisor applies the same rule programmatically to open positions. 
Figure 1.
A single example of the Fisher-style Indicator core behavior: the vertical line marks a point where the oscillator reached a local extreme, visible in the subwindow below the chart. The downward-sloping line on the price chart above traces what happened afterward — a sustained decline following that extreme reading.
Like RSI, the Fisher Transform Style Indicator is often used as a mean-reversion signal: both are built on the idea that price stretched too far in one direction tends to correct back toward the middle. The underlying logic is similar: reach an extreme, expect a reversion. Where the Fisher-style Indicator differs is in how sharply it marks that extreme: because of the logarithmic transform covered in the Theory section, its peaks and troughs tend to be more clearly defined and less prone to the gradual, ambiguous turns that can make an RSI reading harder to time precisely.
Edge Cases
The clamp in Piece 7 (±0.999) means the Fisher value converges toward a fixed ceiling (roughly ±7.6) during a strong, persistent trend, rather than climbing without bound. Since this indicator's subwindow is fixed to -4 to +4, the line can occasionally flatten against the top or bottom edge while the underlying value keeps changing beneath that visual limit.
InpLookbackPeriod has a real effect on behavior, with no universally correct value: shorter periods react faster but pick up more noise, longer periods smooth things out but respond more slowly. The default of 10 is a starting point, not a fixed rule — expect to test a few values for a given symbol or timeframe.
A large price gap (a weekend open, or a sharp move right after news) can push the raw value from one extreme toward the other within a single bar. The two-stage confirmation logic still works correctly here, but the resulting turn can look abrupt rather than the gradual curve seen in typical examples.
In a tightly range-bound market, price can repeatedly touch both edges of its recent high-low window without any genuine directional move. Since normalization is entirely relative to that recent range, this can trigger the ±1.5–2 threshold more often than in a trending market, a limitation revisited in Pros and Cons.
Theory: What the Fisher-style Indicator Actually Computes
Strip away the MQL5 mechanics and the underlying math is three ideas stacked on top of each other.
Idea 1: Normalize price into a bounded range. Take the current close, and express it as a position between the highest high and lowest low over a recent lookback period, rescaled to sit between -1 and +1:
rawValue = 2 × ( (Close − LowestLow) / (HighestHigh − LowestLow) − 0.5 )
Note: this is structurally similar to a Stochastic Oscillator's calculation — both express price as a position within its recent range. The difference is what happens to this value next.
Idea 2: Smooth and clamp the normalized value before transforming it. The raw normalized value is blended with its own previous value to reduce bar-to-bar noise, and clamped just short of -1 and +1:
smoothedValue = 0.5 × (rawValue + rawValue from the previous bar) smoothedValue = clamp(smoothedValue, −0.999, 0.999)
Idea 3: Apply the Fisher-style Indicator itself, smoothed recursively across bars. The bounded, smoothed value is passed through the logarithmic transform, then blended with the previous bar's Fisher value:
Fisher[i] = 0.5 × ln((1 + smoothedValue) / (1 − smoothedValue)) + 0.5 × Fisher[i−1]
In plain terms: values near the center of the recent range produce a Fisher output close to zero, while values near the edges of the range are stretched disproportionately — this is what makes genuine extremes and reversals stand out sharply, instead of blending into the normal back-and-forth of price.
That's the whole indicator. Because the calculation is recursive, the implementation must preserve state across bars. Both the smoothed value (Idea 2) and the Fisher output (Idea 3) depend on the previous bar's result, so these values must be stored and reused across function calls.
Indicator Implementation: Code Walkthrough, Piece by Piece
Here I want to go through the indicator in fragments, in the order execution actually happens, and explain what each piece is doing and why it's written that way. The full compilable file is provided at the end of this article.
Piece 1 — Registering buffers and plot properties
#property indicator_separate_window #property indicator_buffers 2 #property indicator_plots 1 #property indicator_minimum -4 #property indicator_maximum 4 #property indicator_level1 0.0 #property indicator_levelstyle STYLE_DOT #property indicator_label1 "Fisher" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 2 input int InpLookbackPeriod = 10; // Highest/Lowest Lookback Period
This indicator uses indicator_separate_window rather than overlaying the price chart. The Fisher-style Indicator has its own scale, so it should be plotted in a separate subwindow, like RSI or Stochastic. Placing it directly on the price chart wouldn't work at all: its values (roughly -4 to +4) have no meaningful relationship to actual price levels, so overlaying it on candles would just be visual noise. The indicator_minimum/maximum properties fix the subwindow range to -4 to +4. Without this, MetaTrader would auto-scale the window as the chart scrolls, which makes extremes harder to judge visually. Indicator_level1 draws a dotted reference line at zero, giving the reader a fixed visual anchor for where the oscillator sits neutral, rather than having to mentally estimate the middle of the window each time.
There is only one plot: a single line. This is a deliberate simplification — an earlier version of this indicator also plotted buy and sell arrows directly, but that blurred the line between what the indicator observes and what a trader (or an EA) decides to do about it. Keeping the plot count at one keeps that separation clean, a distinction covered in full in the "How to Use It" section further down.
Piece 2 — Buffer declarations, with their own explanatory comments
//--- Indicator output buffer used for drawing the oscillator line. There //--- are deliberately no arrow buffers here — this indicator's only job //--- is to output a clean, continuous value every bar. Deciding when a //--- given value counts as a buy or sell signal (a threshold concept, //--- the same way RSI's 70/30 levels work) belongs to whatever EA reads //--- this indicator, not to the indicator itself. double BufFisher[]; //--- Internal recursive state, registered as a calculation buffer so the //--- terminal manages its size and bar-to-bar continuity automatically. //--- kRawValue stores each bar's normalized value (Idea 1), needed by //--- Idea 2's smoothing step, which blends the current bar's raw value //--- with the previous bar's. BufFisher itself has a value on every bar //--- with no gaps, so it doubles as its own recursive state for Idea 3 — //--- no separate buffer is needed just to look up the previous Fisher //--- value. double kRawValue[];
These two comment blocks are the indicator's own documentation of its data model, and they're worth reading closely rather than skipping past, since they explain a distinction that isn't obvious just from looking at the two declarations side by side. BufFisher holds what actually gets drawn; its values are visible on the chart the moment this indicator is attached. kRawValue exists purely to carry state between bars — nothing about it is ever visible on the chart, but without it, the smoothing step in Piece 7 would have no previous value to blend against, and the whole recursive chain would break.
Registering kRawValue as a calculation buffer matters because MetaTrader preserves its size and values across ticks. With a manually managed array, a resize or reinitialization can accidentally wipe prior values, silently breaking the recursive logic. Registering it properly hands that responsibility to the terminal itself, which is the safer default any time a value genuinely needs to persist from one bar to the next.
Piece 3 — OnInit: binding buffers and validating inputs
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Connect indicator buffers with their corresponding plots SetIndexBuffer(0, BufFisher, INDICATOR_DATA); //--- Register the recursive state as a calculation buffer SetIndexBuffer(1, kRawValue, INDICATOR_CALCULATIONS); //--- Define an empty value so unused buffer points are not drawn PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE); IndicatorSetString(INDICATOR_SHORTNAME, "FisherTransform(" + IntegerToString(InpLookbackPeriod) + ")"); //--- Validate inputs before allowing the indicator to run if(InpLookbackPeriod < 2) { Print("InpLookbackPeriod must be at least 2"); return INIT_PARAMETERS_INCORRECT; } return INIT_SUCCEEDED; }
BufFisher is registered as INDICATOR_DATA, since its values are what actually get drawn. kRawValue is registered as INDICATOR_CALCULATIONS — this hands its sizing and bar-to-bar continuity over to the terminal automatically, rather than requiring manual array management that could silently lose data if handled incorrectly. This single line is doing more work than it might appear to: it's the difference between state that reliably survives across thousands of ticks and state that can quietly corrupt itself the moment the indicator's resizing logic runs at an unexpected time.
The input validation at the end rejects an InpLookbackPeriod below 2 outright, since a highest-high/lowest-low window needs at least two bars to mean anything — a lookback of 0 or 1 would either crash the calculation or produce a meaningless, always-zero range. Rejecting bad inputs immediately in OnInit(), rather than letting the calculation loop discover the problem later, means the user sees a clear error message the moment they try to attach the indicator with an invalid setting, instead of a silently broken or flat-lined chart with no explanation.
Piece 4 — OnCalculate: the guard clause and series setup//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- Check that enough bars exist for the lookback and recursive calculations if(rates_total < InpLookbackPeriod + 3) return 0; //--- Use series indexing where index 0 represents the latest bar. Since //--- kRawValue is a terminal-managed calculation buffer, no manual //--- ArrayResize()/ArrayInitialize() is needed here — the terminal keeps //--- its existing values correctly aligned as new bars appear. ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); ArraySetAsSeries(BufFisher, true); ArraySetAsSeries(kRawValue, true);
The guard clause requires at least InpLookbackPeriod + 3 bars before any calculation runs. This isn't an arbitrary safety margin — the lookback period alone would be enough to compute a single highest-high/lowest-low window, but the recursive nature of the smoothing and Fisher steps further down means the calculation also needs a little breathing room beyond that minimum before its results are meaningful, particularly right after the indicator is first attached to a chart with limited history.
Every array this indicator touches is then explicitly set to series indexing: index 0 is always the newest bar, and higher indexes move backward into history. This line is repeated for every single array the calculation will use, deliberately, rather than relying on some arrays defaulting to one convention and others to another — mixing indexing conventions partway through a calculation is one of the easiest ways to introduce a bug that looks correct on the surface but produces subtly wrong values, since MQL5 won't raise any error if a normal-indexed array and a series-indexed array are read side by side.
Piece 5 — Determining the starting point and seeding on first load
//--- Determine the starting point for the calculation int start = rates_total - prev_calculated; //--- Perform a full initialization on the first calculation if(prev_calculated <= 1) { ArrayInitialize(BufFisher, EMPTY_VALUE); ArrayInitialize(kRawValue, 0.0); //--- Seed the recursive state at the oldest bar the lookback allows int seed = rates_total - InpLookbackPeriod - 1; BufFisher[seed] = 0.0; kRawValue[seed] = 0.0; start = seed - 1; } else { //--- Recalculate the current and most recently closed bar on live ticks if(start < 1) start = 1; }
On a genuine first calculation (prev_calculated <= 1), both buffers reset to zero and a starting point is established at the oldest bar the lookback period actually allows — not the very first bar in the chart's history, since there isn't enough prior data before that point to compute a valid highest-high/lowest-low window. Starting any earlier would mean the very first few calculated values are based on an incomplete window, quietly producing incorrect output right at the beginning of the indicator's history — exactly the kind of subtle correctness bug that's easy to miss in a quick visual check but shows up as wrong signals during backtesting.
On every ordinary tick afterward, only the newest bar or two gets recalculated, which keeps this indicator efficient rather than reprocessing the entire chart's history on every price update. This distinction — a full recalculation exactly once, then cheap incremental updates from then on — is what makes it practical to run an indicator like this on a live chart without noticeable lag, even on symbols with years of historical data loaded.
Piece 6 — The main loop: Idea 1, normalizing price
//--- Main loop: normalize price, smooth it, and apply the Fisher-style Indicator for(int i = start; i >= 0; i--) { //--- Skip invalid oldest index if(i >= rates_total - InpLookbackPeriod) continue; //--- Idea 1: normalize price into a bounded range using the recent //--- highest high and lowest low double highestHigh = high[i]; double lowestLow = low[i]; for(int j = 1; j < InpLookbackPeriod; j++) { if(high[i + j] > highestHigh) highestHigh = high[i + j]; if(low[i + j] < lowestLow) lowestLow = low[i + j]; } double range = highestHigh - lowestLow; double rawValue = (range > 0) ? 2.0 * ((close[i] - lowestLow) / range - 0.5) : 0.0;
The inner loop walks backward across the lookback window to find the highest high and lowest low, and rawValue expresses the current close as a position within that range, rescaled to sit between -1 and +1. A close sitting exactly at the lowest low of the window produces a rawValue of -1; a close at the highest high produces +1; a close sitting in the middle of the range produces something close to 0.
The range > 0 check guards against a divide-by-zero on an illiquid symbol or an unusually flat period — a real scenario, not a theoretical one, since a thinly-traded symbol can genuinely go several bars without the price moving at all, making the highest high and lowest low identical. Without this check, that specific (if rare) condition would produce a division by zero and either crash the calculation or return an undefined value, silently breaking the indicator for that symbol until price eventually moved again.
Piece 7 — Idea 2, smoothing and clamping
//--- Idea 2: smooth the raw value against the previous bar's raw //--- value, then clamp it just short of the ±1 boundary so the //--- upcoming logarithm never receives an invalid input double prevRawValue = kRawValue[i + 1]; double smoothedValue = 0.5 * (rawValue + prevRawValue); if(smoothedValue > 0.999) smoothedValue = 0.999; if(smoothedValue < -0.999) smoothedValue = -0.999; kRawValue[i] = rawValue;
This blends the current bar's raw value with the previous bar's raw value, read directly from the kRawValue buffer. The averaging here exists specifically to reduce bar-to-bar noise before the value ever reaches the logarithmic step — without it, a single unusually sharp price movement could push the raw value briefly toward an extreme and back within one or two bars, which the Fisher-style Indicator would then interpret as a much larger event than it really was, given how aggressively the logarithm stretches values near the edges of the range.
The clamp exists because the next piece applies a logarithm to a term derived from this value — a logarithm that approaches infinity as its input approaches ±1. This is not a hypothetical edge case: a strongly trending market can genuinely push the normalized value right up against its boundary, and without this clamp, that entirely normal market condition would produce an invalid or wildly distorted output at exactly the moment the indicator is supposed to be most useful.
Piece 8 — Idea 3, the Fisher-style Indicator itself
//--- Idea 3: apply the Fisher-style Indicator itself, smoothed recursively //--- against the previous bar's own Fisher value double prevFisher = BufFisher[i + 1]; BufFisher[i] = 0.5 * MathLog((1.0 + smoothedValue) / (1.0 - smoothedValue)) + 0.5 * prevFisher;
The bounded, smoothed value from Piece 7 is passed through the logarithmic transform and blended with the previous bar's own Fisher value. Because BufFisher already holds a continuous value on every bar, prevFisher is read directly from it, with no separate calculation buffer required for it — a deliberate efficiency that avoids storing the same information twice under two different names.
This is the step where the indicator's actual behavior comes from: values near the center of the normalized range pass through the logarithm largely unchanged, while values near the edges are stretched disproportionately, which is precisely what gives the Fisher-style Indicator its characteristic sharp, well-defined peaks and troughs instead of the smoother, more gradual turns typical of an oscillator built from simple averaging alone.
Testing Notes
The indicator was attached to several symbols and timeframes to confirm its behavior matched what the Theory section describes, rather than relying on the math alone. Across all of them, the oscillator consistently produced sharper, more clearly defined peaks and troughs than a simple moving-average-based oscillator would on the same data — the effect the logarithmic transform is meant to produce.
No repainting was observed. Since every value in this indicator depends only on already-closed bars — the highest-high/lowest-low window, the previous raw value, and the previous Fisher value — a confirmed reading at index 1 never changes once that bar has closed, regardless of how much new price data arrives afterward.
The ±1.5–2 threshold zone described in "How to Use It" held up as a reasonable reference across the symbols tested, though consistent with the Edge Cases section, choppier, range-bound periods did produce more frequent threshold touches than clean trending periods did — expected behavior given how the normalization step works, not a flaw specific to any one symbol.
Practical Application: Testing with an Expert Advisor
To demonstrate the indicator's practical use, a simple Expert Advisor was built around the threshold-and-reversal logic described in "How to Use It." The EA reads the Fisher value directly from the indicator via iCustom() — no separate calculation, no duplicated logic — and applies exactly the same two-part rule a trader would apply by eye: wait for the oscillator to reach beyond a threshold, then wait for a confirmed turn back before actually acting.
There is no fixed stop-loss or take-profit here, matching the same design philosophy as the indicator itself: a position is opened on a confirmed reversal signal and held until the next confirmed opposite signal closes it and reverses the position. This keeps the EA's behavior a direct, honest reflection of what the indicator is reporting, rather than adding a separate risk-management layer that would make it harder to judge whether the signal itself is any good.
EA Function Overview
The following section provides a brief overview of the main functions used in the Expert Advisor.
Function 1 — OnInit()
Purpose: Initializes the Expert Advisor, loads the Fisher-style Indicator via iCustom(), and validates that the buy/sell thresholds are set to sensible values before trading begins.
Input: None.
Returns: Returns INIT_SUCCEEDED if initialization is successful; otherwise returns an initialization error code.
Function 2 — OnDeinit()
Purpose: Releases the indicator handle created in OnInit() when the EA is removed from the chart, freeing that terminal resource.
Input: The deinitialization reason code.
Returns: None.
Function 3 — OnTick()
Purpose: Executes the main trading logic once per new bar. It reads the confirmed Fisher value, tracks whether the oscillator is in a threshold zone, and opens or reverses positions on a confirmed reversal.
Input: Current market tick.
Returns: None.
Function 4 — GetFisherValues()
Purpose: Reads the last two confirmed values directly from the indicator via iCustom() and CopyBuffer(). This is the EA's only source of data — no separate calculation exists anywhere in the EA.
Input: References for the current and previous Fisher values.
Returns: true if both values are available; otherwise false.
Function 5 — OpenBuy() / OpenSell()
Purpose: Opens a buy or sell position at the current market price. No stop-loss or take-profit is set; the position is managed entirely by the next confirmed opposite signal.
Input: None.
Returns: None.
Function 6 — CloseAll()
Purpose: Closes any open position belonging to this EA on this symbol, called immediately before opening a position in the opposite direction.
Input: None.
Returns: None.
EA Implementation: Code Walkthrough, Piece by Piece
The EA's job is narrower than the indicator's: read the confirmed Fisher value, track whether it has reached a threshold, and act only once that extreme has genuinely reversed. Nothing here recalculates the oscillator itself — every piece below either reads from the indicator directly or manages the state needed to recognize a confirmed turn, exactly as described in "How to Use It."
Here I want to go through the EA in fragments, in the order execution actually happens, and explain what each piece is doing and why it's written that way. The full compilable file is provided at the end of this article.
Piece 1 — Reading the confirmed signal from the indicator
//+------------------------------------------------------------------+ //| Reads the last two confirmed Fisher values directly from the | //| indicator via iCustom — buffer 0 is the oscillator line | //+------------------------------------------------------------------+ bool GetFisherValues(double ¤t, double &previous) { if(BarsCalculated(hFisher) < 3) return false; double buf[3]; ArraySetAsSeries(buf, true); if(CopyBuffer(hFisher, 0, 0, 3, buf) < 3) return false; //--- Use the last two closed bars (index 1 and 2), matching the same //--- confirmed-value convention used throughout this EA current = buf[1]; previous = buf[2]; return true; }
This function is the EA's only connection to price action, and it deliberately contains no calculation of its own. It reads two values — the last closed bar's Fisher reading and the one before it — because the reversal logic in Piece 6 needs to compare the two directly to detect a turn. The BarsCalculated() check guards against the brief window right after the EA attaches, where the indicator handle exists but hasn't finished its first calculation yet, which would otherwise risk reading incomplete or default buffer values on the very first few ticks.
Piece 2 — Closing positions
//+------------------------------------------------------------------+ //| Closes any open position from this EA on this symbol | //+------------------------------------------------------------------+ void CloseAll() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(!ticket) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue; Trade.PositionClose(ticket); } }
This loops through every open position on the account but only closes the ones matching both this EA's symbol and its magic number — the two checks together make sure it never touches a position belonging to a different EA, a different symbol, or a manual trade placed by the account holder. Looping backward from PositionsTotal() - 1 down to 0 is deliberate as well: closing a position shifts the index of every position after it, so iterating forward while closing would risk skipping one.
Piece 3 — Opening positions
//+------------------------------------------------------------------+ //| Opens a buy at market, no SL/TP — exit happens only on the next | //| confirmed opposite signal | //+------------------------------------------------------------------+ void OpenBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); Trade.Buy(InpLotSize, _Symbol, ask, 0, 0, InpComment); } //+------------------------------------------------------------------+ //| Opens a sell at market, no SL/TP — exit happens only on the next | //| confirmed opposite signal | //+------------------------------------------------------------------+ void OpenSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); Trade.Sell(InpLotSize, _Symbol, bid, 0, 0, InpComment); }
Both functions send a market order with no stop-loss and no take-profit — the position's exit is decided entirely by the next confirmed opposite signal, handled in Piece 6, rather than by a price level calculated separately here. Keeping these two functions this minimal is deliberate: it keeps every part of a trade's lifecycle anchored to the same single source of truth, the Fisher reading itself, instead of splitting that responsibility between the oscillator and a separately-tuned SL/TP distance.
Piece 4 — OnInit: connecting to the indicator
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { Trade.SetExpertMagicNumber(InpMagic); Trade.SetDeviationInPoints(50); hFisher = iCustom(_Symbol, _Period, "The Fisher Transform Style Indicator", InpLookbackPeriod); if(hFisher == INVALID_HANDLE) { Print("Failed to load Fisher Transform Style indicator via iCustom"); return INIT_FAILED; } if(InpBuyThreshold >= 0 || InpSellThreshold <= 0) { Print("InpBuyThreshold must be negative and InpSellThreshold must be positive"); return INIT_PARAMETERS_INCORRECT; } LastState = 0; LastTrend = 0; return INIT_SUCCEEDED; }
The iCustom() call loads the indicator by its exact file name, passing through InpLookbackPeriod so both pieces always run with matching settings instead of silently drifting apart if one were edited without the other. The threshold validation catches a configuration mistake immediately a positive buy threshold or a negative sell threshold would silently break the reversal logic in Piece 6 without ever triggering an obvious error, so it's rejected here instead, at the moment the EA is attached, rather than surfacing later as a strategy that simply never trades.
Piece 5 — OnDeinit: releasing the indicator handle
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(hFisher != INVALID_HANDLE) IndicatorRelease(hFisher); }
This releases the indicator handle created in OnInit() when the EA is removed from the chart. Indicator handles are a limited terminal resource, and skipping this step across repeated attach/detach cycles or many runs during a backtest optimization would leave that resource allocated even after the EA is gone.
Piece 6 — OnTick: the decision flow
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTime = 0; datetime barTime = iTime(_Symbol, _Period, 0); if(barTime == 0 || barTime == lastBarTime) return; lastBarTime = barTime; double current, previous; if(!GetFisherValues(current, previous)) return; //--- On the very first tick, only establish a baseline state — do not //--- allow a signal to fire based on a relationship between bars that //--- existed before the EA started tracking it static bool firstTick = true; if(firstTick) { if(current > InpSellThreshold) LastState = 1; else if(current < InpBuyThreshold) LastState = -1; firstTick = false; return; } //--- Track whether the oscillator is currently sitting beyond a //--- threshold, on either side — a trade only fires once price has //--- genuinely reached an extreme and then confirmed a turn back, not //--- on every minor wiggle near the zero line if(current > InpSellThreshold) LastState = 1; else if(current < InpBuyThreshold) LastState = -1; //--- A confirmed reversal from the overbought zone: close any long and //--- go short if(LastState == 1 && current < previous) { if(LastTrend == 1) CloseAll(); if(LastTrend != -1) { OpenSell(); LastTrend = -1; } LastState = 0; return; } //--- A confirmed reversal from the oversold zone: close any short and //--- go long if(LastState == -1 && current > previous) { if(LastTrend == -1) CloseAll(); if(LastTrend != 1) { OpenBuy(); LastTrend = 1; } LastState = 0; return; } }
A firstTick guard runs before any signal logic: on the very first tick after the EA attaches, it only records the current threshold state as a starting baseline and returns, without evaluating a reversal. This prevents the EA from firing a trade based on a relationship between bars that existed before the EA itself began tracking it.
The static lastBarTime check at the top means the decision logic below only ever runs once per new bar, not on every single tick — an intentional choice that keeps the EA's signal timing tied to the same confirmed, closed-bar values the indicator itself is built around, rather than reacting to intra-bar price noise.
LastState tracks whether the oscillator is currently sitting beyond a threshold — this alone doesn't trigger a trade, it just marks that an extreme has been reached and a reversal is now being watched for. The actual trade only fires in the two blocks below, each requiring both that the state was already set from a prior bar and that the current value has now moved back toward the previous one — the confirmed turn described throughout this article, not a simple threshold crossing.
The if(LastTrend != -1) and if(LastTrend != 1) checks prevent the EA from repeatedly opening the same-direction position if this logic were ever triggered twice in a row without an intervening opposite signal — a safeguard against duplicate entries rather than something expected to fire often in normal use.
EA Testing Notes
The EA was backtested across four instruments — XAU/USD, EUR/USD, GBP/USD, and AUD/USD using the Fisher-style Indicator connected purely via iCustom(), with a $5,000 initial deposit and no fixed stop-loss or take-profit: every position is closed and reversed purely on the next confirmed reversal signal.
XAU/USD:
The strategy was tested on XAU/USD and produced 369 trades. Net profit came in at $996.86, with a profit factor of 1.08 and a recovery factor of 0.51. The equity curve shows a sharp early drawdown in January before recovering and trending upward through the rest of the test window, with several intermediate pullbacks along the way. Maximum balance drawdown reached 32.60%, the highest of the four pairs. Long and short trades performed similarly, with longs winning 62.50% of the time versus 59.46% for shorts.

Figure 2. XAU/USD backtest

Figure 3. XAU/USD Equity Curve
EUR/USD:
On EUR/USD, the strategy produced 439 trades with a net profit of $541.66, a profit factor of 1.14, and a recovery factor of 1.13. The equity curve trends generally upward with a mid-year peak followed by a decline in the final stretch of the test window. Maximum balance drawdown reached 7.59%. Short trades slightly outperformed longs here, winning 66.36% of the time versus 60.73% for longs.

Figure 4. EUR/USD backtest

Figure 5. EUR/USD Equity Curve

GBP/USD:
GBP/USD produced 464 trades with a net profit of $320.38, a profit factor of 1.06, and a recovery factor of 0.43 — the weakest risk-adjusted result among the four pairs, despite still finishing profitable. The equity curve shows a strong rally through the first quarter followed by an extended, choppier decline for the remainder of the test window. Maximum balance drawdown reached 10.96%. Win rates were close between directions, with shorts at 62.07% and longs at 61.21%.

Figure 6. GBP/USD backtest

Figure 7. GBP/USD Equity Curve
AUD/USD:
AUD/USD was the strongest performer among the four pairs by a clear margin. The strategy generated 492 trades, producing a net profit of $1,220.26, with a profit factor of 1.38 and a recovery factor of 3.55 — notably higher than the other three symbols. The equity curve shows a consistent, comparatively smooth upward trend for most of the test window. Maximum balance drawdown reached only 5.60%, the lowest of the four pairs. Long trades performed particularly well, winning 66.67% of the time versus 63.01% for shorts.

Figure 8. AUD/USD backtest

Figure 9. AUD/USD Equity Curve
Summary table by symbol:
| Symbol | Total Trades | Net Profit | Profit Factor | Max Drawdown |
|---|---|---|---|---|
| XAU/USD | 369 | $996.86 | 1.08 | 32.60% |
| EUR/USD | 439 | $541.66 | 1.14 | 7.59% |
| GBP/USD | 464 | $320.38 | 1.06 | 10.96% |
| AUD/USD | 492 | $1,220.26 | 1.38 | 5.60% |
Pros and Cons
Pros:
- The logarithmic transform genuinely delivers sharp, well-defined turning points, rather than the gradual, often-delayed reversals typical of simpler oscillators like a raw moving-average crossover.
- Clean architecture by design — the indicator outputs one honest value with no duplicated logic anywhere, and the EA reads that value directly via iCustom() with no separate calculation of its own.
- The two-stage confirmation rule (reach an extreme, then confirm a reversal) meaningfully reduces noise compared to reacting on every threshold crossing, without adding real complexity to either the indicator or the EA.
- All four symbols tested finished profitable, with AUD/USD showing a particularly strong profit factor (1.38) alongside the lowest drawdown of the group (5.60%) — a reasonable sign the logic isn't overfit to a single instrument.
Cons:
- Like any mean-reversion approach, this indicator assumes price snaps back after stretching too far — an assumption that breaks down during a strong, sustained trend, where the clamp behavior can flatten the oscillator against its display ceiling.
- InpLookbackPeriod has a real effect on results, with no single correct value — what works on one symbol or timeframe may need retuning on another.
- No fixed stop-loss or take-profit. A position stays open until the next confirmed opposite signal, meaning a single adverse move can run further than a trader using fixed risk levels might be comfortable with.
- Drawdown wasn't uniform across symbols — XAU/USD saw a 32.60% maximum balance drawdown, notably higher than the other three pairs, showing the strategy's risk profile can vary meaningfully by instrument even with identical underlying logic.
Conclusion
We delivered a complete, practical pipeline: a compilable Fisher-style indicator and a companion EA that together implement a clear, reproducible signal rule. Concretely, you now have
- an MQL5 indicator that normalizes price over a lookback window, clamps inputs to protect the log step, and stores recursive state in a calculation buffer so values on closed bars never repaint;
- a single output buffer intended solely for charting and EA consumption;
- a formal signal rule—"reach an extreme, then confirm a turn toward zero"—implemented in an EA that reads the indicator via iCustom()/CopyBuffer() and opens/reverses positions only on confirmed closed-bar turns;
- test results across multiple symbols to illustrate where the method tends to succeed and where drawdown can be larger.
Important caveats: the indicator uses a simplified smoothing step, so its numerical traces differ from the classical Fisher Transform; sustained trends can push the indicator toward its display ceiling because of the clamp; and the provided EA intentionally omits fixed stop-loss/take-profit to keep trading logic pure. Treat the EA as a reference implementation: retune InpLookbackPeriod for each instrument/timeframe, and add robust risk controls (position sizing, SL/TP, and out-of-sample testing) before deploying with real capital.
Full Source Code of Indicator:
//+------------------------------------------------------------------+ //| The Fisher Transform Style Indicator.mq5 BY Ali Kazim & Jawad | //+------------------------------------------------------------------+ #property copyright "2026" #property version "2.00" #property strict #property indicator_separate_window #property indicator_buffers 2 #property indicator_plots 1 #property indicator_minimum -4 #property indicator_maximum 4 #property indicator_level1 0.0 #property indicator_levelstyle STYLE_DOT #property indicator_label1 "Fisher" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 2 input int InpLookbackPeriod = 10; // Highest/Lowest Lookback Period //--- Indicator output buffer used for drawing the oscillator line. There //--- are deliberately no arrow buffers here — this indicator's only job //--- is to output a clean, continuous value every bar. Deciding when a //--- given value counts as a buy or sell signal (a threshold concept, //--- the same way RSI's 70/30 levels work) belongs to whatever EA reads //--- this indicator, not to the indicator itself. double BufFisher[]; //--- Internal recursive state, registered as a calculation buffer so the //--- terminal manages its size and bar-to-bar continuity automatically. //--- kRawValue stores each bar's normalized value (Idea 1), needed by //--- Idea 2's smoothing step, which blends the current bar's raw value //--- with the previous bar's. BufFisher itself has a value on every bar //--- with no gaps, so it doubles as its own recursive state for Idea 3 — //--- no separate buffer is needed just to look up the previous Fisher //--- value. double kRawValue[]; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Connect indicator buffers with their corresponding plots SetIndexBuffer(0, BufFisher, INDICATOR_DATA); //--- Register the recursive state as a calculation buffer SetIndexBuffer(1, kRawValue, INDICATOR_CALCULATIONS); //--- Define an empty value so unused buffer points are not drawn PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE); IndicatorSetString(INDICATOR_SHORTNAME, "FisherTransform(" + IntegerToString(InpLookbackPeriod) + ")"); //--- Validate inputs before allowing the indicator to run if(InpLookbackPeriod < 2) { Print("InpLookbackPeriod must be at least 2"); return INIT_PARAMETERS_INCORRECT; } return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- Check that enough bars exist for the lookback and recursive calculations if(rates_total < InpLookbackPeriod + 3) return 0; //--- Use series indexing where index 0 represents the latest bar. Since //--- kRawValue is a terminal-managed calculation buffer, no manual //--- ArrayResize()/ArrayInitialize() is needed here — the terminal keeps //--- its existing values correctly aligned as new bars appear. ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); ArraySetAsSeries(BufFisher, true); ArraySetAsSeries(kRawValue, true); //--- Determine the starting point for the calculation int start = rates_total - prev_calculated; //--- Perform a full initialization on the first calculation if(prev_calculated <= 1) { ArrayInitialize(BufFisher, EMPTY_VALUE); ArrayInitialize(kRawValue, 0.0); //--- Seed the recursive state at the oldest bar the lookback allows int seed = rates_total - InpLookbackPeriod - 1; BufFisher[seed] = 0.0; kRawValue[seed] = 0.0; start = seed - 1; } else { //--- Recalculate the current and most recently closed bar on live ticks if(start < 1) start = 1; } //--- Main loop: normalize price, smooth it, and apply the Fisher Transform Style for(int i = start; i >= 0; i--) { //--- Skip invalid oldest index if(i >= rates_total - InpLookbackPeriod) continue; //--- Idea 1: normalize price into a bounded range using the recent //--- highest high and lowest low double highestHigh = high[i]; double lowestLow = low[i]; for(int j = 1; j < InpLookbackPeriod; j++) { if(high[i + j] > highestHigh) highestHigh = high[i + j]; if(low[i + j] < lowestLow) lowestLow = low[i + j]; } double range = highestHigh - lowestLow; double rawValue = (range > 0) ? 2.0 * ((close[i] - lowestLow) / range - 0.5) : 0.0; //--- Idea 2: smooth the raw value against the previous bar's raw //--- value, then clamp it just short of the ±1 boundary so the //--- upcoming logarithm never receives an invalid input double prevRawValue = kRawValue[i + 1]; double smoothedValue = 0.5 * (rawValue + prevRawValue); if(smoothedValue > 0.999) smoothedValue = 0.999; if(smoothedValue < -0.999) smoothedValue = -0.999; kRawValue[i] = rawValue; //--- Idea 3: apply the Fisher Transform Style itself, smoothed recursively //--- against the previous bar's own Fisher value double prevFisher = BufFisher[i + 1]; BufFisher[i] = 0.5 * MathLog((1.0 + smoothedValue) / (1.0 - smoothedValue)) + 0.5 * prevFisher; } return rates_total; } //+------------------------------------------------------------------+
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.
Automating Chart Patterns in MQL5 (Part 2): The Double Top and Double Bottom
Master MQL5 — From Beginner to Pro (Part VII): Principles of Debugging MQL Applications
Neural Networks in Trading: Generalizing Time Series Without Data-Specific Dependence (Conclusion)
Ebola Optimization Search Algorithm (EOSA)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use