---
title: Regime Filter (4h Trend Gate)
description: >-
  A no-repaint higher-timeframe trend gate that tints candles by regime and
  only marks entries when the confirmed 4h trend agrees with the signal.
---

<div class="flex gap-3 mb-6">
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-medium">
    Intermediate
  </span>
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 text-sm font-medium">
    6 min read
  </span>
</div>

The fastest way to cut false signals is to stop trading against the higher timeframe. This recipe reads a confirmed 4h trend, tints every candle green or red by that regime, and only marks an entry when a fast/slow EMA cross on your chart timeframe lines up with the 4h direction. The key word is *confirmed*: the higher-timeframe values come from `htf()`, which never looks ahead, so a signal that shows up in history would have shown up live, at the same bar. No repaint, no hindsight.

```javascript title="scripts/probes/cookbook/regime_filter.ks" lines wrap
//@version=2
// ============================================================================
//  NO-REPAINT 4H TREND GATE
//  htf() reads a confirmed higher timeframe with no look-ahead. The 4h EMAs
//  are plotted so the script always has visible output; candle tinting and
//  entry markers are added when the confirmed 4h regime is available.
// ============================================================================
define(title="4H Trend Gate (no-repaint)", position="onchart", axis=false);

var fastLen = input(name="fastLen", type="number", defaultValue=9,  label="Fast EMA", constraints={min: 2, max: 100, step: 1});
var slowLen = input(name="slowLen", type="number", defaultValue=21, label="Slow EMA", constraints={min: 5, max: 200, step: 1});
var upCol   = input(name="upCol",   type="color",  defaultValue="#22d3a5", label="Bull Tint");
var dnCol   = input(name="dnCol",   type="color",  defaultValue="#ff5b7f", label="Bear Tint");

timeseries d  = ohlcv(symbol=currentSymbol, exchange=currentExchange);

// Confirmed 4h view of the SAME data: one row per chart bar, no repaint.
timeseries h4 = htf(d, "4h");

var h4Fast = ema(source=h4.close, period=fastLen);
var h4Slow = ema(source=h4.close, period=slowLen);
var bull4h = isnum(h4Fast) && isnum(h4Slow) && h4Fast > h4Slow;
var bear4h = isnum(h4Fast) && isnum(h4Slow) && h4Fast < h4Slow;
timeseries h4FastLine = h4Fast;
timeseries h4SlowLine = h4Slow;

// Chart-timeframe trigger, gated by the higher-timeframe regime.
timeseries fast = ema(source=d.close, period=fastLen);
timeseries slow = ema(source=d.close, period=slowLen);
var crossedUp   = isnum(fast[0]) && isnum(slow[0]) && fast[0] > slow[0] && fast[1] <= slow[1];
var crossedDown = isnum(fast[0]) && isnum(slow[0]) && fast[0] < slow[0] && fast[1] >= slow[1];

plotLine(h4FastLine, colors=[upCol], width=1, label=["4H Fast EMA"], desc=["Confirmed 4h fast EMA"]);
plotLine(h4SlowLine, colors=[dnCol], width=1, label=["4H Slow EMA"], desc=["Confirmed 4h slow EMA"]);

// Tint the actual candles by regime. Needs enough loaded history for the 4h
// slow EMA: roughly slowLen * 4 chart hours on a 1h chart.
if (bull4h) { barcolor(opacity(upCol, 18)); }
if (bear4h) { barcolor(opacity(dnCol, 18)); }

if (crossedUp && bull4h) {
  plotShape(value=d.low[0], shape="triangleup", width=10, colors=[upCol], fill=true, location="belowBar", label=["Long (4h agrees)"], desc=["Chart-timeframe cross up confirmed by the 4h regime"]);
}
if (crossedDown && bear4h) {
  plotShape(value=d.high[0], shape="triangledown", width=10, colors=[dnCol], fill=true, location="aboveBar", label=["Short (4h agrees)"], desc=["Chart-timeframe cross down confirmed by the 4h regime"]);
}
```


## How it works

**The higher-timeframe view.** `htf(d, "4h")` takes the chart's own data and re-buckets it into confirmed 4h bars, returning one value per chart bar. "Confirmed" means a 4h candle only contributes once it has closed, so the line never changes after the fact. This is what makes the gate trustworthy: the green and red tints you see on old bars are exactly what you would have seen live.

**The regime.** Two EMAs on the 4h close define the trend. When the fast 4h EMA is above the slow one the market is in an uptrend (`bull4h`), below it a downtrend (`bear4h`). Both flags `isnum`-guard the EMAs so the warm-up region counts as neither, rather than a coin flip.

**The trigger.** Separately, the script computes the same fast/slow EMAs on your chart timeframe and detects the moment they cross. `crossedUp` is true on the single bar where fast moves from at-or-below slow to above it. That is the entry idea on its own.

**The gate.** The entry only prints when the trigger and the regime agree: `crossedUp && bull4h` for longs, `crossedDown && bear4h` for shorts. A bullish cross during a 4h downtrend is silently dropped, which is the whole point. Most chop happens when you fight the higher timeframe.

**The two render channels.** The script shows its state two ways. `barcolor()` tints every candle by the 4h regime, so the background context is always visible. `plotShape()` drops a triangle only on confirmed, gated entries. The 4h EMA lines are also plotted so there is always something on screen, even before a single cross fires.

> **Render note.** Candle tinting uses the `barcolor` channel. If your chart does not render `barcolor`, the entry triangles and the 4h EMA lines still show, but the green/red candle background will not. The triangle markers carry the same regime information regardless.

## Customize it

- **EMA speed.** `fastLen` and `slowLen` drive both the regime and the trigger. Widen the gap (for example 21 / 55) for fewer, slower signals, tighten it for more. Because the same lengths feed the 4h regime, changing them reshapes the whole gate.
- **Regime timeframe.** `"4h"` is the trend horizon. Bump it to `"1d"` to demand agreement with the daily trend (stricter, fewer entries), or `"1h"` for a looser gate on lower-timeframe charts. Any string `htf()` accepts works. See [multi-timeframe](/kscript/core-concepts/multi-timeframe).
- **Loaded history.** The 4h regime needs enough loaded bars for its slow EMA to warm up, roughly `slowLen * 4` chart hours on a 1h chart. If candles never tint, load more history.
- **Different trigger.** The cross is just one idea. Swap it for an RSI level, a breakout, or any condition you like, and keep the `&& bull4h` / `&& bear4h` gate to filter it by trend.
- **Colors.** `upCol` and `dnCol` set both the tint and the markers, so the chart stays coherent when you recolor.
- **Make it actionable.** Replace each `plotShape` with an [`alert()`](/kscript/functions/alerts) to be notified only on trend-aligned entries.

## Concepts used

- [Multi-timeframe](/kscript/core-concepts/multi-timeframe) for the no-repaint `htf()` regime
- [Moving averages](/kscript/functions/moving-averages) for the `ema` lines and crosses
- [Plotting](/kscript/functions/plotting) for `barcolor`, `plotLine`, and `plotShape`
- [Color functions](/kscript/functions/color-functions) for `opacity` candle tints
