Modular Filter: Recursive States and Pine Script | Pineify
The Modular Filter blends price with its own previous output, updates upper and lower tracking values, and selects a weighted combination using a persistent binary state. This guide describes the exact implementation below: length 14 by default, feedback weight 0.5, and band weight 0.8. Internal state, output slope, and price direction are different quantities.
Inspect the Pine ScriptOrigin and implementation scope
alexgrover published Modular Filter in June 2019, describing feedback and configurable band weighting. The implementation here fixes both weights and exposes only length and source. It is not evidence that Pineify invented the algorithm, nor a claim that every publicly available version produces the same values.
The branch selection is nonlinear, but alpha is constant for a selected length. There is no measured volatility input, learned model, volume filter, or automatic parameter optimization in this script.
Follow the recursion in order
Let x be the selected source, T the filter output, B and C the tracking values, and O the binary state. With alpha = 2 / (length + 1), each bar uses:
- A = 0.5 × x + 0.5 × previous T.
- B = max(A, alpha × A + (1 − alpha) × previous B).
- C = min(A, alpha × A + (1 − alpha) × previous C).
- If A equals B, O = 1. Otherwise, if A equals C, O = 0. Otherwise, retain previous O.
- Upper = 0.8 × B + 0.2 × C; lower = 0.8 × C + 0.2 × B.
- T = upper when O = 1, or lower when O = 0.
The code replaces missing previous T with the current source and missing previous B/C with A. On initialization B = C = A; the first equality branch wins, so O = 1. This initial value does not establish bullish momentum.
B can decrease, and C can increase: they are not monotonic running extrema. The two blends are internal values, not separately plotted bands. This recursion retains history beyond the latest 14 bars; length controls alpha, not a fixed 14-bar window.
Worked example: the output can fall while state stays 1
This hypothetical calculation begins at a constant source of 100, with B = C = T = 100 and O = 1. Length is 14; alpha is 2/15. The EMA comparison also begins at 100 and uses length 14. Values are rounded only for display.
| Source | A | B | C | O | Output T | EMA 14 |
|---|---|---|---|---|---|---|
| 100 | 100 | 100 | 100 | 1 | 100 | 100 |
| 110 | 105 | 105 | 100.666667 | 1 | 104.133333 | 101.333333 |
| 102 | 103.066667 | 104.742222 | 100.986667 | 1 | 103.991111 | 101.422222 |
| 100 | 101.995556 | 104.376 | 101.121185 | 1 | 103.725037 | 101.232593 |
| 90 | 96.862519 | 103.374202 | 96.862519 | 0 | 98.164855 | 99.734914 |
| 98 | 98.082428 | 102.668632 | 97.025173 | 0 | 98.153865 | 99.503592 |
At source 102, A is between B and C, so O remains 1. Yet T falls from 104.133333 to 103.991111 while EMA 14 rises. At source 98 after 90, price rises but T still falls. A state label is therefore not a slope label, and a filter slope is not the current price change.
At source 110, this filter responds more than the EMA. That single response does not establish a universal lead, fewer false signals, or a higher win rate. A flat source and settled states can produce a flat output with O remaining 1 indefinitely; flatness does not require rapid state switching.
Parameters, boundaries, and live bars
Length is a positive integer; source defaults to close. Changing length alters alpha. It does not set a known number of state flips or guarantee a delay measured in bars. Switching to HL2 changes the input sequence; it need not make every response smoother than close.
At length 1, alpha = 1 and both B and C equal A on each valid bar. The first branch always sets O to 1, and T = A = 0.5 × source + 0.5 × previous T. With matching initialization, this is the EMA recursion with alpha 0.5, equivalent to EMA length 3. It is not the unfiltered source.
The current close can change before the bar ends, and the script has no barstate.isconfirmed gate. The blue plot can move intrabar. There are no arrows, dynamic colors, alerts, orders, stops, or performance statistics. The state O is internal to the helper and is not separately exposed by its return value.
For a comparison, use identical symbol, session, chart type, source, history, and bar-close policy. Missing data and history initialization can affect the recursion. A visually smooth trace does not establish that a trading rule is useful.
Define a testable condition before a strategy
Choose whether a proposed event means a price crossing T, a change in T slope, or a change in O. These conditions are not interchangeable. If O is needed, adapt the helper to return it explicitly and compare that result with the intermediate values above.
Specify whether an event must survive the closing update, and whether the order could first execute at that close or a later price. Confirmation prevents a condition from being acted on before its chosen observation time; it does not prevent the next closed bar from reversing it.
For any strategy study, define exits, sizing, transaction costs, and evaluation periods separately. Adding RSI, volume, or another average is an additional hypothesis. No measured improvement or preferred market/timeframe is established by this page.
Execution reference: TradingView bar states and repainting.
Pine Script v6: implementation and limits
Length and source are actual settings inputs. Feedback and beta are fixed inside the helper. The complete example plots one blue line and retains the calculation used in the local indicator helper.
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pineify
//@version=6
indicator(title="Modular Filter", overlay=true, max_labels_count=500)
p_comm_timeframe_to_seconds(simple string tf) =>
float seconds = 0
tf_lower = str.lower(tf)
value = str.tonumber(str.substring(tf_lower, 0, str.length(tf_lower) - 1))
if str.endswith(tf_lower, 's')
seconds := value
else if str.endswith(tf_lower, 'd')
seconds := value * 86400
else if str.endswith(tf_lower, 'w')
seconds := value * 604800
else if str.endswith(tf_lower, 'm')
seconds := value * 2592000
else
seconds := str.tonumber(tf_lower) * 60
seconds
p_custom_sources() =>
[open, high, low, close, volume]
p_ta_mod_filt(series float source, simple int length) =>
z = 0.5
beta = 0.8
ts=0.,b=0.,c=0.,os=0.
alpha = 2/(length+1)
a = z*source + (1-z)*nz(ts[1],source)
b := a > alpha*a+(1-alpha)*nz(b[1],a) ? a : alpha*a+(1-alpha)*nz(b[1],a)
c := a < alpha*a+(1-alpha)*nz(c[1],a) ? a : alpha*a+(1-alpha)*nz(c[1],a)
os := a == b ? 1 : a == c ? 0 : os[1]
upper = beta*b+(1-beta)*c
lower = beta*c+(1-beta)*b
ts := os*upper+(1-os)*lower
// Inputs
length = input.int(14, "Length", minval=1)
src = input.source(close, "Source")
// Calculation
mf_val = p_ta_mod_filt(src, length)
// Plot
plot(mf_val, "MF", color.rgb(41, 98, 255, 0), 1)
Specify a checkable adaptation in Pineify
Describe the source, length, fixed feedback and beta, and whether the requested output is a line, slope event, or internal-state change. Ask the Pine Script AI Coding Agent for an explicit close-confirmation policy if needed. Compare the generated values with the synthetic sequence above and validate any revised code before adding orders.
Open Pine Script AI Coding AgentEducational indicator analysis. No measured trading performance is established here. Syntax validation does not prove formula equivalence, execution assumptions or profitability.