Regime filter (4h trend gate)
A no-repaint higher-timeframe trend gate that tints a line by regime and only marks entries when the confirmed 4h trend agrees with the signal.
The fastest way to cut false signals is to stop trading against the higher timeframe. This recipe reads a confirmed 4h trend, tints its 4h line 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: a 4h candle contributes only once it has closed, so a signal that shows up in history would have shown up live, at the same bar. No repaint, no hindsight.
The wrun indicator
param("fast_len", 9, { min: 2, max: 100, description: "Fast EMA length, on the chart bars and on the 4h buckets" });
param("slow_len", 21, { min: 5, max: 200, description: "Slow EMA length" });
output("h4_fast", line, overlay, {
width: 1,
color_by: "regime",
colors: ["#94a3b8", "#22d3a5", "#ff5b7f"],
description: "Confirmed 4h fast EMA, tinted by regime",
});
output("h4_slow", line, overlay, { color: "#94a3b8", width: 1, description: "Confirmed 4h slow EMA" });
output("regime", none, overlay, { description: "0 warming, 1 bull, 2 bear" });
output("long", shape, overlay, { color: "#22d3a5", shape_where: "is_long" });
output("short", shape, overlay, { color: "#ff5b7f", shape_where: "is_short" });
output("is_long", none);
output("is_short", none);
let h4Fast = new Ema(9);
let h4Slow = new Ema(21);
let fast = new Ema(9);
let slow = new Ema(21);
let cross = new Cross();
let bucket: f64 = NaN;
let bucketClose: f64 = NaN;
let h4FastValue: f64 = NaN;
let h4SlowValue: f64 = NaN;
function onStart(): void {
h4Fast = new Ema(i32(p_fast_len()));
h4Slow = new Ema(i32(p_slow_len()));
fast = new Ema(i32(p_fast_len()));
slow = new Ema(i32(p_slow_len()));
cross = new Cross();
}
function onBar(): void {
const close = bar.close();
const high = bar.high();
const low = bar.low();
// 4h buckets on UTC boundaries. A bucket's close folds into the 4h EMAs only once the NEXT bucket
// has started, so the regime never uses a 4h candle that is still forming: no repaint.
const b = Math.floor(bar.time() / 14400.0);
if (b != bucket) {
if (!isNaN(bucketClose)) {
h4FastValue = h4Fast.update(bucketClose);
h4SlowValue = h4Slow.update(bucketClose);
}
bucket = b;
}
bucketClose = close;
const regime = isNaN(h4FastValue) || isNaN(h4SlowValue)
? 0.0
: h4FastValue > h4SlowValue
? 1.0
: h4FastValue < h4SlowValue
? 2.0
: 0.0;
const crossed = cross.update(fast.update(close), slow.update(close));
if (isNaN(h4SlowValue)) return;
out_h4_fast(h4FastValue);
out_h4_slow(h4SlowValue);
out_regime(regime);
out_long(low);
out_short(high);
out_is_long(crossed == 1 && regime == 1.0 ? 1.0 : 0.0);
out_is_short(crossed == -1 && regime == 2.0 ? 1.0 : 0.0);
}How it works
The higher-timeframe view. Math.floor(bar.time() / 14400) puts every chart bar in a UTC 4h bucket. The module remembers the last close it saw in the current bucket, and when a bar from the NEXT bucket arrives it folds that remembered close into the two 4h EMAs. A 4h candle therefore contributes exactly once, after it closed. That is what makes the gate trustworthy: the tints you see on old bars are what you would have seen live. The promise is fourteen lines you can read, and the Execution model explains why an indicator cannot break it by accident.
The regime. Fast 4h EMA above slow is bull (1), below is bear (2), and warming is 0. It is a data-only output, and the h4_fast line indexes its three-entry palette with it: grey, green, red. Both 4h EMAs are guarded with isNaN so the warm-up region counts as neither regime, rather than a coin flip.
The trigger. Separately, the module computes the same fast/slow EMAs on the chart timeframe and detects the moment they cross. Cross.update(fast, slow) returns +1 on the one bar where the fast EMA moves from at-or-below to above the slow one, -1 for the opposite, 0 otherwise, including on any bar where either side is still NaN. That is the entry idea on its own.
The gate. is_long is 1 only when the cross and the regime agree; long is a shape output at the bar's low, gated by it. A bullish cross during a 4h downtrend is silently dropped, which is the whole point: most chop happens when you fight the higher timeframe.
Two ways to see the state. The 4h line carries the regime through color_by, so the context is always visible, and the two shape outputs drop a mark only on confirmed, gated entries. The 4h EMA lines are drawn even before a single cross fires, so there is always something on screen.
Design notes
- The higher timeframe is built in the module from the chart's own bars, and the confirm-on-next-bucket rule is the no-repaint contract written out. The chart also serves the direct form, an
interval: "4h"pin on a secondcloseinput: a 4h leg of closed candles fetched on its own, read as of each row's close, on any chart whose interval divides 4h evenly. It sees each 4h close one bar earlier than the bucketing does, on the bar whose close ends the 4h candle, and both are causal (Multi-timeframe). - The regime tint is a choice. This indicator tints the 4h line through
color_by;render.barcolortints the candles themselves with the same gate and ladder options, and a background tint per bar isrender.bgcolor(Plotting). - A
shapeoutput's value is where its mark sits:longis written the bar's low, so the mark hangs below the candle, andshortthe bar's high, so it sits above. - The colors are
colorsandcoloron the declarations: a palette on the line thatcolor_byindexes, one color on each mark.
Customize it
- EMA speed.
fast_lenandslow_lendrive both the regime and the trigger. Widen the gap (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. Change
14400.0to86400.0for a daily regime (stricter, fewer entries) or3600.0for a looser gate on lower-timeframe charts. Bucketing works for any multiple of the chart interval. - Loaded history. The 4h regime needs enough loaded bars for its slow EMA to warm up, roughly
slow_len * 4chart hours on a 1h chart. If the line never tints, pan back: the indicator reruns over the longer window. - Different trigger. Swap the cross for an RSI level or a breakout and keep the
regime == 1.0/regime == 2.0gates to filter it by trend. - Make it actionable.
is_longis data-only, so the alert dialog does not offer it as a condition. Bind it to aconst(const isLong = output("is_long", none);) and declarealert("trend_long", { when: isLong, message: "{{symbol}} trend-aligned long" }): once the indicator is published and on a chart, that signal is in the chart's alert dialog and fires only on trend-aligned entries (Alerts).
Run it
- In the editor's Explorer, press New indicator and pick Blank indicator: the tab holds the
//@lang=wrun-tsline alone. - Paste the indicator block above under that line and press Run on a chart finer than 4h, such as 15m or 1h.
- At the editor's Console prompt, type
last 20 regimeto read the regime of the last 20 bars.
Concepts used
- Multi-timeframe for the no-repaint bucket fold and the
intervalpin the chart serves - Moving averages for
Emaand Series functions forCross - Styling for
color_byover a data-only output - Repainting for why a bucket folds in only on the next bucket's first bar