- MQL5 75.4%
- MQL4 15.8%
- Python 8.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| docs | ||
| LICENSES | ||
| mql4 | ||
| mql5 | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||

Range Filter Signals (MetaTrader 5 and MetaTrader 4)
A free range-filter indicator that draws a trend line which moves only when price travels further than a smoothed average range, two optional bands, and one buy or sell arrow per turn, on closed bars only, with an alert for each arrow. Source for both editions, a Python reference of the maths, and the tests that were used to check that the MT5 and MT4 code, the Python reference and each other all agree.
Website: allanninal.dev/mql5 (indicators) · MT4 editions · allanninal.dev · Contact: message me on LinkedIn.
It is an indicator, not a strategy. It draws a line, bands and arrows and places no trades. Nothing here is a profit claim (see Honest limits).
Get it on the MQL5 Market
| Edition | Platform | Price | Link |
|---|---|---|---|
| Range Filter Signals | MT5 | free | https://www.mql5.com/en/market/product/201871 |
| Range Filter Signals MT4 | MT4 | free | https://www.mql5.com/en/market/product/202329 |
The Market builds are the same calculation as the source in this repository.
Contents
- What it is and who it is for
- Why it was built
- How it works
- Non-repainting: the proof and how it was tested
- Architecture and buffers
- Inputs, alerts, install and compile
- Run the Python reference and the tests
- Honest limits
- Licence and credits
What it is and who it is for
What: the "range filter" method as a native MetaTrader indicator. The line follows the close only when the close moves
further than a range away from it: a close above the line pulls the line up to close - range (never down), a close below
pulls it down to close + range (never up), and smaller moves leave it flat. The range is the average size of a
bar-to-bar close change, smoothed twice and multiplied. When a buy setup follows a sell setup (or the reverse) you get an
arrow (buy below the candle, sell above) and an alert (pop-up, sound, push, e-mail).
For: traders who want a trend line that ignores small noise, on any symbol and timeframe, and who want to know that the
arrow they saw will still be there tomorrow. Also for developers who want a small, tested indicator with documented
buffers to read from an EA with iCustom.
Not for: anyone who expects arrows to predict direction. They mark where a close moved further than a volatility-scaled range from the line.
![]() |
![]() |
![]() |
![]() |
Image notes: the charts are drawn by the Python reference on broker history (the same numbers the indicator shows). They
are illustrations, not terminal captures. MT4 versions are in docs/images/mt4/.
Why it was built
The Range Filter is one of the most-liked trend scripts on TradingView, and traders who move to MetaTrader ask for it. The motivations, stated with the evidence we actually have:
- A native, readable port. The method is a few lines of arithmetic and does not need a closed-source box.
- A gap in the free tier. Our market research (October 2026) found large interest on TradingView (the original script and its variants have thousands of likes), only two paid range-filter indicators on the MQL5 Market (each with no reviews at the time) and no free one.
- Trust in the arrows. The same research found repeated complaints about arrow indicators that repaint, and a 2026-07 request for "arrows confirmed on bar close". The design rule follows: an arrow is only drawn on a bar that has closed, and never moves afterwards.
- Honest scale of demand. Named classic indicators are requested only rarely by name on forums and freelance boards (about one Reddit post mentioning Range Filter in the scan). So this is a free tool that gives a clean reference implementation and builds a track record, not a claim of mass demand.
- Provable behaviour. Every number in this README comes from an executed check, listed below, not from a promise.
mindmap
root((Range Filter Signals))
Problem
Popular TradingView line, little on MetaTrader
Arrow indicators that repaint
Alerts that fire twice or during history load
Design rules
Signals on closed bars only
One alert per bar
Same maths on MT5 MT4 and Python
Proof
Python reference
Bar by bar parity
Tester repaint probe
Market validation
How it works
The maths, step by step
Let C[k] be the close of the k-th bar fed to the engine (oldest = 0), p the sampling period and m the multiplier.
-
Average change
d[k] = |C[k] - C[k-1]|fork >= 1, smoothed with an EMA of periodp:avrng[k] = avrng[k-1] + 2/(p+1) * (d[k] - avrng[k-1]), seeded with the firstd. -
Smoothed range an EMA of
avrngwith period2p - 1:smrng[k] = smrng[k-1] + 2/(2p) * (avrng[k] - smrng[k-1]), seeded with the firstavrng. Thenr[k] = smrng[k] * m. -
Filter
f[k]: it starts at the first close that has a range (f = C[1]). After that:- if
C[k] > f[k-1]:f[k] = max(f[k-1], C[k] - r[k])(it only rises) - otherwise:
f[k] = min(f[k-1], C[k] + r[k])(it only falls)
so it moves only when price travels more than
raway from it. The bands aref +/- r. - if
-
Run counters:
upcounts bars in a row the filter has risen,downbars in a row it has fallen; an unchanged filter keeps both. The trend side is+1whileup > 0,-1whiledown > 0. -
Setups: a buy setup is
C[k] > f[k],C[k] != C[k-1]andup > 0; a sell setup isC[k] < f[k],C[k] != C[k-1]anddown > 0. The engine remembers the last setup (cond: buy, sell or none). -
Signal (closed bars only): a buy arrow when a buy setup arrives while the last setup was a sell; a sell arrow when a sell setup arrives while the last was a buy. The first setup after the start gives no arrow. One arrow per turn.
The engine is fed closed bars only, in order, starting InpHistory bars before the last closed bar; the forming bar is
never fed, so the line and the arrows on closed bars never change.
Per-bar algorithm
flowchart TD
A[Next closed bar close C] --> B{First bar fed?}
B -- yes --> Z[Remember C, no line yet]
B -- no --> C1[d = abs of C minus previous C]
C1 --> D[avrng = EMA of d over p<br/>smrng = EMA of avrng over 2p-1<br/>range r = smrng x m]
D --> E{C above the filter?}
E -- yes --> F[filter = max of filter, C - r]
E -- no --> G[filter = min of filter, C + r]
F --> H[Update the rising and falling run counters]
G --> H
H --> I{Buy setup? C above filter, C moved, filter rising}
I -- yes --> J[setup = buy]
I -- no --> K{Sell setup? C below filter, C moved, filter falling}
K -- yes --> L[setup = sell]
K -- no --> M[setup unchanged]
J --> N{Previous setup was sell?}
L --> O{Previous setup was buy?}
N -- yes --> P[Buy arrow on the bar]
O -- yes --> Q[Sell arrow on the bar]
N -- no --> R[No arrow]
O -- no --> R
M --> R
P --> S[Alert once per bar, only when live]
Q --> S
Signal states
stateDiagram-v2
[*] --> NoSetup
NoSetup --> BuySetup: first buy setup (no arrow)
NoSetup --> SellSetup: first sell setup (no arrow)
BuySetup --> BuySetup: another buy setup (no arrow)
SellSetup --> SellSetup: another sell setup (no arrow)
BuySetup --> SellSetup: sell setup (sell arrow, alert)
SellSetup --> BuySetup: buy setup (buy arrow, alert)
Alert flow
sequenceDiagram
participant T as Terminal
participant I as Range Filter indicator
participant H as Alert helper
participant U as You
T->>I: OnCalculate (history load, prev_calculated = 0)
I->>I: rebuild the engine from the last InpHistory closed bars
Note over I,H: no alert during the history load
T->>I: OnCalculate (new bar closes, prev_calculated > 0)
I->>I: feed the bar that just closed
alt it gave a signal and its time is newer than the last alert
I->>H: SignalAlertMessage(text)
alt not in the tester
H->>U: pop-up (optional sound, push, e-mail)
end
else no signal or already alerted
I-->>T: nothing sent
end
How often it signals (measured)
Signals (buy plus sell) per month with the default inputs (period 100, multiplier 3.0), on closed bars of broker history,
October 2023 to October 2026 (D1: ten years). Source: docs/data/arrows-per-month.json.
This counts arrows only. It says nothing about profit.
xychart-beta
title "Range filter signals per month by timeframe (EURUSD and XAUUSD)"
x-axis ["M15", "H1", "H4", "D1"]
y-axis "signals per month" 0 --> 70
line [60.89, 15.42, 4.06, 0.65]
line [57.67, 14.78, 3.56, 0.75]
First line EURUSD, second XAUUSD.
xychart-beta
title "XAUUSD signals per month, default inputs"
x-axis ["M15", "H1", "H4", "D1"]
y-axis "signals per month" 0 --> 70
bar [57.67, 14.78, 3.56, 0.75]
Non-repainting: the proof and how it was tested
"Non-repainting" is easy to claim. Here is what was actually run. Raw result files are in tests/results/.
1. Three implementations, compared bar by bar. The Python reference
(tests/reference.py) is a separate implementation of the same maths. The MT5 and MT4 indicators
were dumped bar by bar (arrows, filter, bands, side, range, setup state) in the MetaTrader Strategy Tester, and
tests/check_parity.py compared each dump with the reference. Pass means: filter, range and
bands within 1e-9 relative, side and setup identical, and arrows on exactly the same bars.
| MT5 | MT4 | |
|---|---|---|
| dumps compared | 24 (4 symbols x M15/H1/H4 x 2 input sets) | 25 (same 24 + one every-tick EURUSD H1) |
| bars compared | 72,000 | 514,102 |
| arrows compared (buy + sell) | 3,072 | 21,437 |
| result | ALL PASS | ALL PASS |
Symbols: EURUSD, XAUUSD, GBPJPY, BTCUSD. Input sets: defaults (period 100, multiplier 3.0); period 20, multiplier 2.0.
The tests below were re-run on exactly the code in this repository. Source: tests/results/parity-mt5-and-mt4.txt. The MT5 dumps cover the indicator's
default window of 3,000 closed bars each, which is why the MT5 bar count is much smaller than the MT4 one (the MT4
test calculates the whole test history).
xychart-beta
title "Arrows compared, Python vs indicator (all 0 differences)"
x-axis ["Defaults MT5", "Period 20 MT5", "Defaults MT4", "Period 20 MT4"]
y-axis "arrows compared" 0 --> 15000
bar [1102, 1970, 7655, 13782]
(MT4 "Defaults" is 7,479 arrows over 12 runs plus 176 in the every-tick run = 7,655.)
2. Repaint probe. An EA records which signal buffers are set on each bar at the moment it closes, then reads the same bars again at the end and counts any value that changed. It also counts any signal ever shown on the still-forming bar.
| Run | Closed bars checked | Signals seen at close | Changed afterwards | Signals on the live bar |
|---|---|---|---|---|
| MT5, XAUUSD H1, every tick, 91,052,717 ticks | 4,406 | 131 | 0 | 0 |
| MT4, 25 runs combined | 505,320 | 21,079 | 0 | 0 |
Source: tests/results/repaint-summary.md. The MT5 repaint probe was run once (XAUUSD
H1, default inputs); the MT4 probe ran on every dump run.
3. Market validation. Both Market builds passed the MQL5 Market's automated product validation before publication (this is the Market's own check, not part of this repository).
flowchart LR
R[reference.py<br/>Python, same maths] --> P
S5[MT5 indicator] --> T5[Tester script<br/>DumpIndicator<br/>every bar to CSV]
S4[MT4 indicator] --> T4[Tester EA<br/>RangeFilterDump + Dump4.mqh<br/>CSV + repaint log]
T5 --> P[check_parity.py<br/>1e-9, same arrows]
T4 --> P
S5 --> RP[RepaintProbe EA<br/>values at close vs final]
T4 --> RL[repaint log<br/>per run]
P --> V{all pass?}
RP --> V
RL --> V
V -- yes --> M[Market validation]
V -- no --> F[fix, rerun everything]
Why the design cannot repaint
- The engine is fed closed bars only: the loop stops at
rates_total - 2, so the forming bar is never used. - Each step depends only on the previous state and the new close (recursive formulas, no look-ahead).
- Alerts fire only for the bar that just closed, only after the history load, once per bar, and never in the tester.
- The MT4 tester recalculates an indicator from scratch at the end of a run, and a fresh calculation covers only the last
InpHistorybars; the MT4 test therefore stores each bar's values as the live instance produced them and compares those.
Architecture and buffers
flowchart TB
subgraph MT5["mql5"]
A5[Indicators/RangeFilter/RangeFilterSignals.mq5<br/>inputs, buffers, OnCalculate]
E5[Include/RangeFilter/RF.mqh<br/>engine: RFInit, RFFeed, RFSide]
B5[Include/RangeFilter/SignalAlerts.mqh<br/>alert inputs, SignalAlertMessage]
A5 -- include --> E5
A5 -- include --> B5
end
subgraph MT4["mql4/Indicators/RangeFilter"]
A4[RangeFilterSignals.mq4]
E4[RF.mqh<br/>same engine as MT5]
B4[SignalAlerts.mqh<br/>MT4 edition]
A4 -- include --> E4
A4 -- include --> B4
end
subgraph TESTS[tests]
R[reference.py]
C[check_parity.py]
D5[mql5: DumpIndicator, RepaintProbe, Params.mqh]
D4[mql4: RangeFilterDump.mq4, Dump4.mqh]
C --> R
end
D5 -. iCustom/IndicatorCreate .-> A5
D4 -. iCustom .-> A4
classDiagram
class RangeFilterSignals {
+InpPeriod int
+InpMult double
+InpBands bool
+InpHistory int
+OnInit()
+OnCalculate()
}
class RF {
+RFParams period, mult
+RFState filt, range, cond, ev
+RFInit()
+RFFeed()
+RFSide()
}
class SignalAlerts {
+InpAlertPopup bool
+InpAlertSound bool
+InpAlertPush bool
+InpAlertEmail bool
+SignalAlertMessage()
}
class reference {
+run()
}
RangeFilterSignals --> RF : engine
RangeFilterSignals --> SignalAlerts : alerts
reference ..> RF : same maths, compared by check_parity
iCustom buffer table (for EA developers)
| Buffer | MT5 | MT4 |
|---|---|---|
| 0 | Buy arrow price (bar low), empty otherwise | Buy arrow price, empty otherwise |
| 1 | Sell arrow price (bar high), empty otherwise | Sell arrow price, empty otherwise |
| 2 | range filter line | range filter line |
| 3 | colour index of the line (0 rising, 1 falling, 2 flat) | rising part of the drawn line |
| 4 | upper band | upper band |
| 5 | lower band | lower band |
| 6 | trend side (+1, -1, 0) | trend side |
| 7 | range r |
range r |
| 8 | last setup (+1 buy, -1 sell, 0 none) | last setup |
| 9-10 | not used | falling part and flat part of the drawn line |
Empty value is EMPTY_VALUE. Read arrows on closed bars (shift 1 and higher). iCustom parameters follow the input
order: sampling period, range multiplier, draw the bands, closed bars to calculate, arrow gap (MT4 only), pop-up, sound,
sound file, push, e-mail, debug. In MT5 the input group headings also count as string parameters when you build a handle with
IndicatorCreate (see tests/mql5/Include/IndTest/Params.mqh).
Inputs, alerts, install and compile
| Input | Default | Meaning |
|---|---|---|
| Sampling period | 100 | Smoothing length of the average range (2 to 1000) |
| Range multiplier | 3.0 | Scales the range. Larger: fewer, later signals |
| Draw the bands | true | Show or hide the two dotted bands |
| Closed bars to calculate | 3000 | How much history is calculated (300 to 50000) |
| Arrow distance from the candle (MT4 only) | 0.3 x ATR(14) | Gap between arrow and candle (MT5 uses a fixed pixel gap) |
| Pop-up alert | true | Terminal alert window |
| Play a sound / sound file | false / alert.wav | File from the terminal's Sounds folder |
| Push notification | false | Needs your MetaQuotes ID in Options |
| false | Needs e-mail set up in Options | |
| Debug log for bug reports | false | Writes extra lines to the Experts tab, to attach when you report a problem |
Alert text: Range Filter Signals: BUY on EURUSD H1 at 1.08512.
Install from source. MT5: copy mql5/Indicators/RangeFilter/ into <terminal data folder>/MQL5/Indicators/ and
mql5/Include/RangeFilter/ into <data folder>/MQL5/Include/, open RangeFilterSignals.mq5 in MetaEditor and press
Compile (F7). MT4: copy mql4/Indicators/RangeFilter/ into <data folder>/MQL4/Indicators/ (the three files of the
edition stay together) and compile RangeFilterSignals.mq4. Both compile with 0 errors and 0 warnings (checked on a
Windows machine with MetaEditor; the compiled files are not in the repository). The debug log needs the Chart Kit file as well: copy mql5/Include/ChartKit/ (MT5) or mql4/Include/ChartKit/ (MT4) into the Include folder of the data folder.
Run the Python reference and the tests
python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt
import sys; sys.path.insert(0, "tests")
from reference import run
# c is a list of closing prices of closed bars, oldest first
rows = run(c, period=100, mult=3.0) # per bar: filt, range, up, dn, cond, ev (+1 buy, -1 sell), side
To repeat the parity check on your own data:
- Compile the indicator and the test tool for your platform (
tests/mql5/Scripts/DumpIndicator, ortests/mql4/RangeFilterDump.mq4withtests/mql4/Include/IndTest4/Dump4.mqhin yourMQL4/Include/IndTest4/; MT5 also needstests/mql5/Include/IndTest/Params.mqh). - Run it in the Strategy Tester (MT4) or as a script (MT5) for each symbol and timeframe, with the input sets in
tests/configs.tsv(MT5) /tests/mql4/configs.tsv(MT4). The tool writes one CSV per symbol and timeframe. python3 tests/check_parity.py <folder>where the CSVs are in<folder>/dump/and namedrf_default_*orrf_p20_m2_*(add--mt4for MT4 dumps). It prints PASS or FAIL per file and an overall result.- Repaint: run
tests/mql5/Experts/RepaintProbein the MT5 Strategy Tester (every tick); MT4 writes its repaint log with the dump.
The price dumps used for the published numbers are not in this repository (they are broker price data).
Honest limits
- An indicator signal is not a strategy. No stop-loss, position sizing or exit rules are provided or implied, and the arrows were not optimised or back-tested for profit. Whether a turn of the filter makes money depends on your market, costs and rules.
- No profit or accuracy claim is made. The only numbers here are about consistency (the three implementations agree, signals do not move) and frequency (signals per month).
- Lag is built in. The filter moves only after price has travelled more than the range away; a longer period or larger multiplier gives fewer signals that come later still.
- Sideways markets leave the line flat, but a flat line can still produce a turn when price finally breaks away.
- Window-dependent. The EMAs are seeded with the first values the engine sees, and the engine starts
Closed bars to calculatebars back, so the first signals of a window can differ from a longer one. - Scope of the tests. Four symbols, three timeframes, two input sets, one MT5 repaint run. Other symbols and timeframes are not covered by the published numbers. The MT5 build was not run on a live chart as part of these numbers.
Licence and credits
Origin of the maths. The range-filter method is popularised by TradingView open-source scripts, notably "Range Filter [DW]" (the initials in the title are the author's) and its many variants. Thank you to the authors.
What this means for the licence. TradingView publishes open-source scripts under the Mozilla Public License 2.0 unless the author states another licence. I did not check the original script's own header, so I took the conservative route: the files that implement the range-filter calculation are licensed MPL-2.0:
mql5/Include/RangeFilter/RF.mqhmql4/Indicators/RangeFilter/RF.mqhmql5/Indicators/RangeFilter/RangeFilterSignals.mq5mql4/Indicators/RangeFilter/RangeFilterSignals.mq4tests/reference.py
They are independent implementations in other languages, written from the maths, plus additions (closed-bar signals, alerts), but treating them as derived from an MPL-2.0 work is the safe reading. MPL-2.0 is file-level: changes to those files must stay MPL-2.0 and be shared.
Everything else (alert helpers SignalAlerts.mqh, test tools, parity checker, docs, images) is © 2026 Allan Niñal under the
PolyForm Noncommercial License 1.0.0: free for personal and other
noncommercial use, not for commercial use (source-available, not OSI open source). Each source file has an
SPDX-License-Identifier line. Texts: LICENSE, LICENSES/MPL-2.0.txt. The licence
applies to this source, not to anything you build or trade with the indicator. Trading involves risk; the software is
provided as is.
Website: allanninal.dev/mql5 · MT4 · allanninal.dev · Questions: message me on LinkedIn.




