Appearance
Personalize your experience
Volatility-Targeted Trend
FreeKalman trend that steps aside when volatility is unreliable
Live engine preview
The QuaScript engine's own output on the chart the terminal uses. Change the settings or edit the code and press Run to recompute it.
About
A trend follower that steps aside when volatility is unreliable. The direction comes from a pair of Kalman-filtered averages; a position is held only while the Yang-Zhang volatility is below its long-run percentile cap and the five OHLC estimators agree, which removes the gap- and crash-driven periods where trend systems give back most of their gains. Entry: fast Kalman line above (below) the slow one, volatility percentile below the cap, and estimator disagreement below the limit. Exit: direction flip, a volatility spike, or a 4 ATR stop. Risk: 95 % of equity while invested, 30 % drawdown halt. Method: Kalman (1960); Yang and Zhang (2000) volatility.
Settings
Fast Kalman noise ratio: 0.01
Slow Kalman noise ratio: 0.0006
Volatility window: 30
Percentile history: 500
Volatility cap (percentile): 80
Maximum disagreement: 0.45
Stop (ATR): 4
Measured backtests
Run by the backtest engine on public candles, 2025-11-01 to 2026-11-01, with the script's own commission and slippage, on BTCUSDT 1h (Binance) and ETHUSDT 1h (Binance). The figures per market are in the Measured backtest panel below. Past results do not predict future returns.
Backtest it yourself
Open it on the backtesting page to run it on any symbol, interval and date range, then use Monte Carlo, optimisation, walk-forward and robustness tests.
Strategy Rules
Entry Conditions
- ▸ fast Kalman line above (below) the slow one, volatility percentile below the cap, and estimator disagreement below the limit
Exit Conditions
- ▸ direction flip, a volatility spike, or a 4 ATR stop
Measured backtest
BTCUSDT 1h (Binance), 2025-11-01 to 2026-11-01 · backtest engine, script costs included
-30.3%
Return
-2.34
Sharpe
-30.4%
Max DD
15.4%
Win Rate
26
Trades
| Market | Trades | Return | Sharpe | Max DD | Win rate | Profit factor |
|---|---|---|---|---|---|---|
| BTCUSDT 1h | 26 | -30.3% | -2.34 | -30.4% | 15.4% | 0.19 |
| ETHUSDT 1h | 18 | -29.9% | -1.84 | -30.3% | 22.2% | 0.24 |
Past results on historical data do not predict future returns.
Source Code
Edit in IDE| 1 | strategy "Volatility-Targeted Trend" { |
| 2 | overlay = true |
| 3 | capital = 100000 |
| 4 | commission = 0.05 |
| 5 | slippage = 1 |
| 6 | sizing = percent_equity |
| 7 | size = 95 |
| 8 | pyramiding = 1 |
| 9 | } |
| 10 | /// A trend follower that steps aside when volatility is unreliable. The direction comes from a |
| 11 | /// pair of Kalman-filtered averages; a position is held only while the Yang-Zhang volatility is |
| 12 | /// below its long-run percentile cap and the five OHLC estimators agree, which removes the gap- and |
| 13 | /// crash-driven periods where trend systems give back most of their gains. |
| 14 | /// Entry: fast Kalman line above (below) the slow one, volatility percentile below the cap, and |
| 15 | /// estimator disagreement below the limit. Exit: direction flip, a volatility spike, or a 4 ATR stop. |
| 16 | /// Risk: 95 % of equity while invested, 30 % drawdown halt. |
| 17 | /// Method: Kalman (1960); Yang and Zhang (2000) volatility. |
| 18 | use "viz/premium" as pr |
| 19 | fast_q = param.float(0.01, title = "Fast Kalman noise ratio", min = 0.00001, max = 1, step = 0.0005, group = "Trend") |
| 20 | slow_q = param.float(0.0006, title = "Slow Kalman noise ratio", min = 0.000001, max = 0.5, step = 0.0001, group = "Trend") |
| 21 | vol_len = param.int(30, title = "Volatility window", min = 5, max = 500, group = "Volatility") |
| 22 | rank_len = param.int(500, title = "Percentile history", min = 50, max = 5000, group = "Volatility") |
| 23 | cap = param.float(80, title = "Volatility cap (percentile)", min = 30, max = 100, group = "Volatility") |
| 24 | dis_max = param.float(0.45, title = "Maximum disagreement", min = 0.05, max = 2, step = 0.05, group = "Volatility") |
| 25 | stop_k = param.float(4, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1, group = "Risk") |
| 26 | // process / measurement noise ratio sets the responsiveness (about EMA 20 and EMA 80 here) |
| 27 | f = ta.kalman(close, fast_q, 1) |
| 28 | s = ta.kalman(close, slow_q, 1) |
| 29 | yz = hft.yang_zhang(vol_len, 252) |
| 30 | pk = hft.parkinson(vol_len, 252) |
| 31 | gk = hft.garman_klass(vol_len, 252) |
| 32 | rs = hft.rogers_satchell(vol_len, 252) |
| 33 | cc = hft.realized_vol(close, vol_len, 252) |
| 34 | mean_v = (yz + pk + gk + rs + cc) / 5 |
| 35 | dis = (math.max(math.max(math.max(yz, pk), math.max(gk, rs)), cc) - math.min(math.min(math.min(yz, pk), math.min(gk, rs)), cc)) / math.max(mean_v, 1e-12) |
| 36 | pct = stat.percent_rank(yz, rank_len) |
| 37 | // hysteresis: enter below the cap, leave only well above it, so the filter does not flicker |
| 38 | persist calm = false |
| 39 | if pct < cap and dis < dis_max { calm := true } |
| 40 | if pct > math.min(cap + 12, 100) or dis > dis_max * 1.4 { calm := false } |
| 41 | a = ta.atr(20) |
| 42 | // direction changes only once the lines separate by half an ATR |
| 43 | persist dir = 0 |
| 44 | if f - s > 0.5 * a { dir := 1 } |
| 45 | if s - f > 0.5 * a { dir := -1 } |
| 46 | want = calm ? dir : 0 |
| 47 | // one trade per direction signal: after a stop, wait for the next flip instead of re-entering |
| 48 | persist used = 0 |
| 49 | if dir != dir[1] { used := 0 } |
| 50 | if want == 1 and position.side <= 0 and used != 1 { |
| 51 | order.market("L", side.long) |
| 52 | exit.bracket("L exit", entry = "L", stop = close - stop_k * a) |
| 53 | used := 1 |
| 54 | } |
| 55 | if want == -1 and position.side >= 0 and used != -1 { |
| 56 | order.market("S", side.short) |
| 57 | exit.bracket("S exit", entry = "S", stop = close + stop_k * a) |
| 58 | used := -1 |
| 59 | } |
| 60 | if want == 0 and position.size != 0 { exit.close_all() } |
| 61 | risk.max_drawdown(30) |
| 62 | pf = render.line(f, "Fast Kalman", color = calm ? (f > s ? color.emerald.300 : color.rose.300) : color.slate.500, width = 2) |
| 63 | ps = render.line(s, "Slow Kalman", color = color.translucent(color.slate.300, 40), width = 1.5) |
| 64 | render.band(pf, ps, color = color.translucent(f > s ? color.emerald.500 : color.rose.500, calm ? 85 : 97), opacity = 1, title = "Trend ribbon") |
| 65 | persist t = draw.table.create(anchor.top_right, 5, 2) |
| 66 | if bar.is_last { |
| 67 | pr.heading(t, 2, "Volatility-Targeted Trend", "Strategy · Kalman trend, YZ volatility") |
| 68 | pr.row(t, 2, "Volatility percentile", str.format("{:.0f}", pct), pct < cap ? pr.BULL : pr.WARN) |
| 69 | pr.row(t, 3, "Estimator disagreement", str.format("{:.0%}", dis), dis < dis_max ? pr.BULL : pr.WARN) |
| 70 | pr.row(t, 4, "Stance", want > 0 ? "Long" : want < 0 ? "Short" : "Stand aside", color.white) |
| 71 | } |
| 72 |
Reviews
No reviews yet. Be the first to review this listing.