Appearance
Personalize your experience
Variance Ratio Breakout
FreeKeltner breakouts only when variance ratios say momentum pays
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
Takes breakouts only when they statistically tend to continue. The Lo-MacKinlay variance ratio at the holding period compares the variance of q-bar returns with q times the one-bar variance: above one, moves extend; below one, they reverse. Breakouts of a volatility-scaled channel are traded only when the ratio says momentum pays, and held for about the holding period. Entry: close beyond the Keltner channel with VR(q) above the threshold. Exit: ATR stop, a target of `reward` R, or a time stop after twice the holding period. Risk: 1 % per trade. Method: Lo and MacKinlay (1988).
Settings
Holding period (bars): 8
Estimation window: 200
Minimum variance ratio: 1.1
Channel length: 20
Channel width (ATR): 2
Stop (ATR): 1.8
Target (R): 2.5
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
- ▸ close beyond the Keltner channel with VR(q) above the threshold
Exit Conditions
- ▸ ATR stop, a target of `reward` R, or a time stop after twice the holding period
Measured backtest
BTCUSDT 1h (Binance), 2025-11-01 to 2026-11-01 · backtest engine, script costs included
-8.4%
Return
-1.17
Sharpe
-15.2%
Max DD
29.3%
Win Rate
41
Trades
| Market | Trades | Return | Sharpe | Max DD | Win rate | Profit factor |
|---|---|---|---|---|---|---|
| BTCUSDT 1h | 41 | -8.4% | -1.17 | -15.2% | 29.3% | 0.68 |
| ETHUSDT 1h | 55 | -3.3% | -0.33 | -9.6% | 34.5% | 0.89 |
Past results on historical data do not predict future returns.
Source Code
Edit in IDE| 1 | strategy "Variance Ratio Breakout" { |
| 2 | overlay = true |
| 3 | capital = 100000 |
| 4 | commission = 0.05 |
| 5 | slippage = 1 |
| 6 | sizing = risk_percent |
| 7 | max_leverage = 1 |
| 8 | size = 1 |
| 9 | pyramiding = 1 |
| 10 | } |
| 11 | /// Takes breakouts only when they statistically tend to continue. The Lo-MacKinlay variance ratio |
| 12 | /// at the holding period compares the variance of q-bar returns with q times the one-bar variance: |
| 13 | /// above one, moves extend; below one, they reverse. Breakouts of a volatility-scaled channel are |
| 14 | /// traded only when the ratio says momentum pays, and held for about the holding period. |
| 15 | /// Entry: close beyond the Keltner channel with VR(q) above the threshold. Exit: ATR stop, a |
| 16 | /// target of `reward` R, or a time stop after twice the holding period. |
| 17 | /// Risk: 1 % per trade. Method: Lo and MacKinlay (1988). |
| 18 | use "viz/premium" as pr |
| 19 | q = param.int(8, title = "Holding period (bars)", min = 2, max = 60, group = "Test") |
| 20 | vr_len = param.int(200, title = "Estimation window", min = 50, max = 3000, group = "Test") |
| 21 | vr_min = param.float(1.1, title = "Minimum variance ratio", min = 0.8, max = 3, step = 0.01, group = "Test") |
| 22 | kc_len = param.int(20, title = "Channel length", min = 5, max = 200, group = "Entry") |
| 23 | kc_mult = param.float(2.0, title = "Channel width (ATR)", min = 0.5, max = 5, step = 0.1, group = "Entry") |
| 24 | stop_k = param.float(1.8, title = "Stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk") |
| 25 | reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk") |
| 26 | vr = hft.variance_ratio(close, vr_len, q) |
| 27 | (kc_mid, kc_up, kc_lo) = ta.keltner(close, kc_len, kc_mult, kc_len) |
| 28 | a = ta.atr(20) |
| 29 | momentum = vr > vr_min |
| 30 | flat = position.size == 0 |
| 31 | if flat and momentum and close > kc_up and close[1] <= kc_up[1] { |
| 32 | order.market("L", side.long) |
| 33 | exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = close + stop_k * a * reward) |
| 34 | } |
| 35 | if flat and momentum and close < kc_lo and close[1] >= kc_lo[1] { |
| 36 | order.market("S", side.short) |
| 37 | exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = close - stop_k * a * reward) |
| 38 | } |
| 39 | if position.size != 0 and position.bars_held >= 2 * q { exit.close_all() } |
| 40 | risk.max_drawdown(25) |
| 41 | pu = render.line(kc_up, "Channel high", color = color.translucent(color.amber.300, 40), width = 1) |
| 42 | pl = render.line(kc_lo, "Channel low", color = color.translucent(color.amber.300, 40), width = 1) |
| 43 | render.band(pu, pl, color = momentum ? color.amber.500 : color.slate.600, opacity = 0.06, title = "Channel") |
| 44 | render.line(kc_mid, "Channel mid", color = momentum ? color.amber.300 : color.slate.500, width = 1.5) |
| 45 | persist t = draw.table.create(anchor.top_right, 4, 2) |
| 46 | if bar.is_last { |
| 47 | pr.heading(t, 2, "Variance Ratio Breakout", "Strategy · Lo-MacKinlay filter") |
| 48 | pr.row(t, 2, str.format("VR({})", q), str.format("{:.3f}", vr), momentum ? pr.BULL : pr.LABEL) |
| 49 | pr.row(t, 3, "Breakouts", momentum ? "Traded" : "Ignored", momentum ? pr.BULL : pr.LABEL) |
| 50 | } |
| 51 |
Reviews
No reviews yet. Be the first to review this listing.