AlgoPro UniversityCourse home
Part 4 · Lesson

Signals, filters & regimes

A raw signal fires everywhere; a good strategy only fires where its edge lives. Learn to wrap a base signal in trend, volatility, session and regime filters — and to avoid the trap of conflicting rules.

By now you can generate a signal. The problem is that a raw signal is indiscriminate — a mean-reversion rule will happily fade a runaway trend into oblivion, and a crossover will whipsaw itself to death in a dead range. The fix is a filter: a second condition that does not create trades, it *permits* them. Filters are how you tell a strategy "your edge is real, but only here".

The four filters you will use most

  1. Trend filter — only take longs when a long-horizon average is rising (e.g. price above its 200-bar MA). This is the single most valuable filter in trading: it stops mean-reversion longs during downtrends and aligns momentum trades with the bigger move.
  2. Volatility filter — only trade when volatility is in a workable band. Too quiet and moves cannot pay for costs; too wild and stops get blown out. ATR or rolling standard deviation measures it.
  3. Regime filter — classify the market as *trending* or *ranging* and switch which strategy is even allowed to run. The ADX indicator or an efficiency ratio is the usual tool.
  4. Session / time filter — only trade during liquid hours (London/New York overlap for FX) or avoid known landmines (the minutes around a rate decision, the weekend gap).

A base signal plus a regime filter

Let us make it concrete. We take the z-score mean-reversion signal from the last lesson — which is dangerous in a trend — and gate it behind a trend/regime filter so it only fires when the market is genuinely ranging. We measure "how trending is this?" with an *efficiency ratio*: net movement divided by total path length. A ratio near 1 means a straight, efficient trend; near 0 means choppy, directionless price — mean-reversion heaven.

pythonA mean-reversion signal gated by a trend/regime filter
import numpy as np
import pandas as pd

def efficiency_ratio(close: pd.Series, window: int = class="n">20) -> pd.Series:
    class="s">""class="s">"Kaufman efficiency ratio: net change / sum of absolute changes.
    ~class="n">1 = clean trend, ~class="n">0 = choppy range. A regime thermometer."class="s">""
    net = close.diff(window).abs()
    path = close.diff().abs().rolling(window).sum()
    return net / path

def filtered_reversion(df: pd.DataFrame, lookback: int = class="n">20,
                       entry: float = class="n">2.0, exit: float = class="n">0.5,
                       max_er: float = class="n">0.35) -> pd.DataFrame:
    class="s">""class="s">"z-score reversion that is ONLY allowed to trade in ranging regimes."class="s">""
    out = df.copy()
    ma = out[class="s">"close"].rolling(lookback).mean()
    sd = out[class="s">"close"].rolling(lookback).std()
    out[class="s">"z"]  = (out[class="s">"close"] - ma) / sd
    out[class="s">"er"] = efficiency_ratio(out[class="s">"close"], lookback)

    class="c"># FILTER: only permit trades when the market is choppy (er below threshold)
    ranging = out[class="s">"er"] < max_er

    position = np.zeros(len(out)); holding = class="n">0
    z, ok = out[class="s">"z"].values, ranging.values
    for i in range(len(out)):
        if holding == class="n">0 and ok[i]:                 class="c"># signal AND filter passes
            if   z[i] <= -entry: holding = class="n">1
            elif z[i] >=  entry: holding = -class="n">1
        elif holding == class="n">1 and z[i] >= -exit:  holding = class="n">0
        elif holding == -class="n">1 and z[i] <=  exit: holding = class="n">0
        class="c"># Note: we let open trades EXIT even if the regime flips —
        class="c"># you never trap yourself in a position because a filter changed.
        position[i] = holding
    out[class="s">"position"] = pd.Series(position, index=out.index).shift(class="n">1).fillna(class="n">0)
    return out

Regimes, drawn

The reason the filter matters is that markets visibly switch character. The same instrument spends weeks trending, then months chopping. The efficiency ratio simply puts a number on which mode you are in so your code can react to it:

One instrument, two regimes
A clean trend (left half) followed by a choppy range (right half). The reversion strategy is muzzled through the trend and only wakes up when price starts oscillating — where its edge actually exists.

The trap: conflicting signals

The moment you have more than one rule, you can generate contradictions — one condition says long, another says short, and naive code either flips wildly bar to bar or takes both. This is a top source of silent bugs and phantom backtest returns. Three disciplines keep you safe:

Filters decide *whether* to trade. The final piece is *how* the trade is run once it is open — the entry, the stop, the target and the exit. That is where most of your actual return is decided, and it is the last lesson of the part.