
QuaScript Documentation
Language guide, engine API, 601 builtins and the preset library · v3.0.0-phase1
Strategies
Adaptive Band Breakout
strategy · presets/strategy/adaptive_band_breakout.qs
Closes beyond ATR bands whose width adapts to the volatility percentile, taken in the direction of the basis slope; exits on a close back through the basis or at the ATR stop.
strategy "Adaptive Band Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Closes beyond ATR bands whose width adapts to the volatility percentile, taken in the direction
/// of the basis slope; exits on a close back through the basis or at the ATR stop.
len = param.int(34, title = "Basis length", min = 2, max = 500)
lo_k = param.float(1.2, title = "Tight multiplier", min = 0.2, max = 10, step = 0.1)
hi_k = param.float(3.0, title = "Wide multiplier", min = 0.5, max = 15, step = 0.1)
stopk = param.float(2.0, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1)
basis = ta.ema(close, len)
a = ta.atr(len)
rank = stat.percent_rank(a, 250) / 100
k = lo_k + (hi_k - lo_k) * math.coalesce(rank, 0.5)
up = basis + k * a
dn = basis - k * a
flat = position.size == 0
if flat and close > up and basis > basis[1] and not math.is_na(a) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stopk * a)
}
if flat and close < dn and basis < basis[1] and not math.is_na(a) {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stopk * a)
}
if position.size > 0 and close < basis { exit.close_all() }
if position.size < 0 and close > basis { exit.close_all() }
risk.max_drawdown(25)
pu = render.line(up, "Upper", color = color.translucent(color.rose.300, 40), width = 1)
render.line(basis, "Basis", color = color.amber.300, width = 1.5)
pd = render.line(dn, "Lower", color = color.translucent(color.emerald.300, 40), width = 1)
render.band(pu, pd, color = color.indigo.500, opacity = 0.05, title = "Adaptive band")Anchored VWAP Reclaim
strategy · presets/strategy/anchored_vwap_reclaim.qs · uses viz/premium
Trades the moment the crowd that bought the last swing low is back in profit. A VWAP is anchored at every confirmed swing low and swing high. In an uptrend, a pullback that closes below the swing-low VWAP puts those buyers under water; when price closes back above it within a few bars, the dip was absorbed and the buyers defend their average. Shorts mirror this with the swing-high VWAP in a downtrend. Entry: trend filter (close beyond a rising 200-bar EMA), a close at least flush ATR below the newest swing-low VWAP, then a close back above it by the margin within window bars (mirror for shorts). Exit: stop beyond the dip extreme (at least 1 ATR), target reward R, trailing exit on a close back through the VWAP. Risk: 1 % per trade.
strategy "Anchored VWAP Reclaim" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trades the moment the crowd that bought the last swing low is back in profit. A VWAP is anchored
/// at every confirmed swing low and swing high. In an uptrend, a pullback that closes below the
/// swing-low VWAP puts those buyers under water; when price closes back above it within a few
/// bars, the dip was absorbed and the buyers defend their average. Shorts mirror this with the
/// swing-high VWAP in a downtrend.
/// Entry: trend filter (close beyond a rising 200-bar EMA), a close at least `flush` ATR below the
/// newest swing-low VWAP, then a
/// close back above it by the margin within `window` bars (mirror for shorts). Exit: stop beyond
/// the dip extreme (at least 1 ATR), target `reward` R, trailing exit on a close back through the
/// VWAP. Risk: 1 % per trade.
use "viz/premium" as pr
left = param.int(10, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right = param.int(5, title = "Swing bars right", min = 1, max = 30, group = "Swings")
trend_n = param.int(200, title = "Trend EMA", min = 20, max = 1000, group = "Filter")
window = param.int(6, title = "Reclaim window (bars)", min = 1, max = 50, group = "Signals")
margin = param.float(0.15, title = "Reclaim margin (ATR)", min = 0, max = 2, step = 0.05, group = "Signals")
flush = param.float(0.3, title = "Flush depth (ATR)", min = 0, max = 3, step = 0.05, group = "Signals")
reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
pl = ta.pivot_low(left, right)
ph = ta.pivot_high(left, right)
pv = bar.typical * volume
persist lp = na
persist lv = na
persist hp = na
persist hv = na
lp := lp + pv
lv := lv + volume
hp := hp + pv
hv := hv + volume
sp = 0.0
sv = 0.0
if not math.is_na(pl) or not math.is_na(ph) {
for i in 0..=right {
sp := sp + pv[i]
sv := sv + volume[i]
}
}
if not math.is_na(pl) {
lp := sp
lv := sv
}
if not math.is_na(ph) {
hp := sp
hv := sv
}
vl = lp / lv
vh = hp / hv
ema = ta.ema(close, trend_n)
a = ta.atr(14)
slope = (ema - ema[20]) / math.max(a, 1e-12)
up = close > ema and slope > 0.5
dn = close < ema and slope < -0.5
below = close < vl - flush * a
above = close > vh + flush * a
since_below = ta.bars_since(below)
since_above = ta.bars_since(above)
dip_low = ta.lowest(low, window + 1)
pop_high = ta.highest(high, window + 1)
go_l = up and close > vl + margin * a and not below and math.coalesce(since_below, 1000000) <= window and since_below >= 1
go_s = dn and close < vh - margin * a and not above and math.coalesce(since_above, 1000000) <= window and since_above >= 1
// one trade per reclaim: re-arm once price trades through the line again
persist used_l = false
persist used_s = false
if below { used_l := false }
if above { used_s := false }
flat = position.size == 0
if flat and go_l and not used_l {
stop = math.min(dip_low, close - a)
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = stop, target = close + (close - stop) * reward)
used_l := true
}
if flat and go_s and not used_s {
stop = math.max(pop_high, close + a)
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = stop, target = close - (stop - close) * reward)
used_s := true
}
if position.size > 0 and close < vl - a { exit.close("L") }
if position.size < 0 and close > vh + a { exit.close("S") }
risk.max_drawdown(25)
render.line(ema, "Trend EMA", color = color.translucent(color.slate.300, 50), width = 1)
render.line(vl, "Swing-low VWAP", color = color.emerald.300, width = 1.75)
render.line(vh, "Swing-high VWAP", color = color.rose.300, width = 1.75)
render.mark(go_l and flat and not used_l[1], shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Reclaim", title = "Long reclaim")
render.mark(go_s and flat and not used_s[1], shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "Reject", title = "Short rejection")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Anchored VWAP Reclaim", "Strategy · swing-anchored VWAPs")
pr.row(t, 2, "Trend", up ? "Up" : "Down", up ? pr.BULL : pr.BEAR)
pr.row(t, 3, "Swing-low VWAP", str.format("{:.2f} ({:+.2%})", vl, close / vl - 1), close > vl ? pr.BULL : pr.BEAR)
pr.row(t, 4, "Swing-high VWAP", str.format("{:.2f} ({:+.2%})", vh, close / vh - 1), close > vh ? pr.BULL : pr.BEAR)
}Aurora Ignition
strategy · presets/strategy/aurora_ignition.qs · uses viz/premium
Trades the first expansion out of a quiet market. Volatility (ATR over price) is ranked against its own history; a rank under the quiet level marks stored energy. When, soon after such a compression, price closes outside the second volatility curtain around a two-pole smoothed centre while the rank is rising, the expansion has started. The position rides the centre line: the stop trails at the centre once the trade is a full ATR in profit. Entry: rank below quiet within the last memory bars, close beyond centre +/- 2 curtains (width ATR each), rank rising. Exit: initial stop at the centre (at least 1 ATR), trailing stop at the centre after +1 ATR, close back inside the first curtain on the wrong side, or extreme volatility rank (take the move). Risk: 1 % per trade.
strategy "Aurora Ignition" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trades the first expansion out of a quiet market. Volatility (ATR over price) is ranked against
/// its own history; a rank under the quiet level marks stored energy. When, soon after such a
/// compression, price closes outside the second volatility curtain around a two-pole smoothed
/// centre while the rank is rising, the expansion has started. The position rides the centre line:
/// the stop trails at the centre once the trade is a full ATR in profit.
/// Entry: rank below `quiet` within the last `memory` bars, close beyond centre +/- 2 curtains
/// (`width` ATR each), rank rising. Exit: initial stop at the centre (at least 1 ATR), trailing
/// stop at the centre after +1 ATR, close back inside the first curtain on the wrong side, or
/// extreme volatility rank (take the move). Risk: 1 % per trade.
use "viz/premium" as pr
len = param.int(34, title = "Centre smoothing", min = 5, max = 400, group = "Aurora")
atr_n = param.int(20, title = "ATR length", min = 2, max = 400, group = "Aurora")
width = param.float(0.6, title = "Curtain spacing (ATR)", min = 0.1, max = 3, step = 0.05, group = "Aurora")
rank_n = param.int(250, title = "Volatility rank window", min = 30, max = 5000, group = "Regime")
quiet = param.float(15, title = "Quiet rank (%)", min = 1, max = 50, group = "Regime")
hot = param.float(97, title = "Take-profit rank (%)", min = 60, max = 100, group = "Regime")
memory = param.int(20, title = "Compression memory (bars)", min = 2, max = 200, group = "Signals")
mid = ta.super_smoother(close, len)
a = ta.atr(atr_n)
rank = math.coalesce(stat.percent_rank(a / close, rank_n), 50)
since_quiet = ta.bars_since(rank < quiet)
recent = not math.is_na(since_quiet) and since_quiet <= memory
up2 = mid + 2 * width * a
dn2 = mid - 2 * width * a
go_l = recent and close > up2 and rank > rank[1]
go_s = recent and close < dn2 and rank > rank[1]
persist used = false
if rank < quiet { used := false }
flat = position.size == 0
if flat and go_l and not used {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = math.min(mid, close - a))
used := true
}
if flat and go_s and not used {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = math.max(mid, close + a))
used := true
}
// trail at the centre once a full ATR in profit
if position.size > 0 and close - position.avg_price > a { exit.bracket("L stop", entry = "L", stop = mid) }
if position.size < 0 and position.avg_price - close > a { exit.bracket("S stop", entry = "S", stop = mid) }
if position.size > 0 and close < mid - width * a { exit.close("L") }
if position.size < 0 and close > mid + width * a { exit.close("S") }
if position.size != 0 and rank > hot { exit.close_all() }
risk.max_drawdown(25)
c = render.line(mid, "Centre", color = color.translucent(color.white, 35), width = 1.5)
o_up = render.line(up2, "Ignition line above", color = color.translucent(color.amber.300, 40), width = 1)
o_dn = render.line(dn2, "Ignition line below", color = color.translucent(color.amber.300, 40), width = 1)
glow = color.ramp(rank, 0, 100, "magma", 70)
render.band(o_up, c, color = glow, opacity = 1, title = "Upper aurora")
render.band(c, o_dn, color = glow, opacity = 1, title = "Lower aurora")
render.mark(go_l and flat and not used[1], shape = shape.triangle_up, at = anchor.below_bar, color = color.amber.300, text = "Ignite", title = "Long ignition")
render.mark(go_s and flat and not used[1], shape = shape.triangle_down, at = anchor.above_bar, color = color.orange.400, text = "Ignite", title = "Short ignition")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Aurora Ignition", "Strategy · compression to expansion")
pr.row(t, 2, "Volatility rank", str.format("{:.0f} %", rank), rank < quiet ? color.violet.300 : rank > hot ? pr.WARN : color.white)
pr.row(t, 3, "Compression", recent ? str.format("{} bars ago", since_quiet) : "None recent", recent ? pr.BULL : pr.LABEL)
pr.row(t, 4, "Armed", recent and not used ? "Yes" : "No", recent and not used ? pr.BULL : pr.LABEL)
}Auto Channel Reversion
strategy · presets/strategy/auto_channel_reversion.qs · uses viz/premium
Trades the edges of the regression channel that fits price best right now. On every bar twelve windows (geometric ladder) are fitted and the one with the highest R squared is the channel; trades are only taken while that fit is clean, so the edges mean something. Entry: in a rising channel, the low pierces the lower k-sigma edge and the bar closes back inside (mirror for falling channels). Exit: target on the centre line, stop beyond the (k + 1)-sigma edge, time stop, and an exit when the channel's slope flips. Risk: 1 % of equity per trade.
strategy "Auto Channel Reversion" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Trades the edges of the regression channel that fits price best right now. On every bar twelve
/// windows (geometric ladder) are fitted and the one with the highest R squared is the channel;
/// trades are only taken while that fit is clean, so the edges mean something.
/// Entry: in a rising channel, the low pierces the lower k-sigma edge and the bar closes back inside
/// (mirror for falling channels). Exit: target on the centre line, stop beyond the (k + 1)-sigma
/// edge, time stop, and an exit when the channel's slope flips. Risk: 1 % of equity per trade.
use "viz/premium" as pr
l_min = param.int(30, title = "Shortest window", min = 10, max = 500, group = "Channel")
l_max = param.int(400, title = "Longest window", min = 50, max = 3000, group = "Channel")
q_min = param.float(0.6, title = "Minimum R squared", min = 0.1, max = 0.95, step = 0.01, group = "Channel")
k_in = param.float(2.0, title = "Entry edge (sigma)", min = 0.5, max = 4, step = 0.1, group = "Entry")
hold = param.int(50, title = "Maximum bars held", min = 2, max = 1000, group = "Exit")
ratio = math.pow(l_max / l_min, 1 / 11)
br = 0.0
bv = na
bs = na
bsd = na
for k in 0..=11 {
len = math.round(l_min * math.pow(ratio, k))
(v, s, _) = stat.linreg(close, len, 0)
r2 = stat.r_squared(close, len)
sd = stat.stdev(close - v, len)
if r2 > br {
br := r2
bv := v
bs := s
bsd := sd
}
}
clean = br > q_min
lo = bv - k_in * bsd
hi = bv + k_in * bsd
flat = position.size == 0
long_sig = clean and bs > 0 and low <= lo and close > lo
short_sig = clean and bs < 0 and high >= hi and close < hi
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = bv - (k_in + 1) * bsd, target = bv)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = bv + (k_in + 1) * bsd, target = bv)
}
flip = (position.size > 0 and bs < 0) or (position.size < 0 and bs > 0)
if position.size != 0 and (position.bars_held >= hold or flip) { exit.close_all() }
risk.max_drawdown(25)
pu = render.line(hi, "Upper edge", color = color.translucent(color.amber.300, 40), width = 1)
pc = render.line(bv, "Channel centre", color = clean ? (bs > 0 ? color.sky.300 : color.amber.300) : color.slate.500, width = 2)
pl = render.line(lo, "Lower edge", color = color.translucent(color.sky.300, 40), width = 1)
render.band(pu, pl, color = clean ? (bs > 0 ? color.sky.500 : color.amber.500) : color.slate.600, opacity = 0.07, title = "Channel")
render.mark(long_sig and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, size = 0.7, title = "Edge buy")
render.mark(short_sig and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.amber.300, size = 0.7, title = "Edge sell")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Auto Channel Reversion", "Strategy · best-fit channel edges")
pr.row(t, 2, "R squared", str.format("{:.2f}", br), clean ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Channel", clean ? (bs > 0 ? "Clean rising" : "Clean falling") : "Noisy: no trades", color.white)
}Autocorrelation Fade
strategy · presets/strategy/autocorrelation_fade.qs · uses viz/premium
Fades outsized one-bar moves, but only while one-bar returns are significantly negatively autocorrelated, i.e. while the market has been giving back its moves. Outside that regime the same moves are left alone. Trades are short-lived by design. Entry: lag-1 autocorrelation below minus the significance band, and the bar's return beyond k standard deviations; the fade is against that bar. Exit: target half the bar's range back, ATR stop, or after hold bars. Risk: 1 % per trade. Method: sample autocorrelation with the 2 / sqrt(n) significance band.
strategy "Autocorrelation Fade" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Fades outsized one-bar moves, but only while one-bar returns are significantly negatively
/// autocorrelated, i.e. while the market has been giving back its moves. Outside that regime the
/// same moves are left alone. Trades are short-lived by design.
/// Entry: lag-1 autocorrelation below minus the significance band, and the bar's return beyond
/// k standard deviations; the fade is against that bar. Exit: target half the bar's range back,
/// ATR stop, or after `hold` bars. Risk: 1 % per trade.
/// Method: sample autocorrelation with the 2 / sqrt(n) significance band.
use "viz/premium" as pr
len = param.int(150, title = "Autocorrelation window", min = 30, max = 3000, group = "Regime")
k_sd = param.float(1.8, title = "Move size (sigma)", min = 0.5, max = 5, step = 0.1, group = "Entry")
hold = param.int(6, title = "Maximum bars held", min = 1, max = 100, group = "Exit")
stop_k = param.float(1.5, title = "Stop (ATR)", min = 0.3, max = 6, step = 0.1, group = "Risk")
r = math.log(close / close[1])
ac = stat.autocorr(r, len, 1)
sd_now = stat.stdev(r, len)
sd = sd_now[1]
sig = 2 / math.sqrt(len)
reverting = ac < -sig
a = ta.atr(20)
big_up = r > k_sd * sd
big_dn = r < -k_sd * sd
flat = position.size == 0
if flat and reverting and big_dn {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = close + 0.5 * (high - low))
}
if flat and reverting and big_up {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = close - 0.5 * (high - low))
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
render.mark(reverting and big_dn, shape = shape.circle, at = anchor.below_bar, color = color.lime.300, size = 0.6, title = "Fade down move")
render.mark(reverting and big_up, shape = shape.circle, at = anchor.above_bar, color = color.violet.300, size = 0.6, title = "Fade up move")
render.background(reverting ? color.translucent(color.lime.500, 95) : na)
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Autocorrelation Fade", "Strategy · lag-1 reversal regime")
pr.row(t, 2, "Lag-1 autocorrelation", str.format("{:+.3f}", ac), reverting ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Regime", reverting ? "Reverting: fading" : "No edge", color.white)
}Bollinger Mean Reversion
strategy · presets/strategy/bollinger_reversion.qs
Fades closes outside the 2-sigma band when RSI agrees, but only while the Hurst exponent says the market is mean reverting. Exits at the basis or on a 3-sigma stop.
strategy "Bollinger Mean Reversion" {
overlay = true
capital = 50000
commission = 0.04
sizing = percent_equity
size = 50
}
/// Fades closes outside the 2-sigma band when RSI agrees, but only while the Hurst exponent
/// says the market is mean reverting. Exits at the basis or on a 3-sigma stop.
len = param.int(20, title = "Band length", min = 5)
k = param.float(2.0, title = "Band width", min = 0.5, max = 5, step = 0.1)
h_max = param.float(0.5, title = "Max Hurst", min = 0.3, max = 0.7, step = 0.01)
(mid, up, dn) = ta.bollinger(close, len, k)
sd = (up - mid) / k
r = ta.rsi(close, 14)
reverting = stat.hurst(close, 100) < h_max
if position.size == 0 and reverting {
if close < dn and r < 30 {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = mid - 3 * sd, target = mid)
}
if close > up and r > 70 {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = mid + 3 * sd, target = mid)
}
}
pu = render.line(up, "Upper", color = color.slate.500)
pd = render.line(dn, "Lower", color = color.slate.500)
render.line(mid, "Basis", color = color.amber.400)
render.band(pu, pd, color = color.sky.500, opacity = 0.06)
render.background(reverting ? na : color.translucent(color.slate.500, 92))Book Gravity Reversion
strategy · presets/strategy/book_gravity_reversion.qs
Fades stretches away from the order book's average centre of gravity (20-bar EMA): when the close is more than the threshold (in ATR) below the size-weighted centre of the top levels and the book still leans to the bids, buy for a return to the centre; mirror for shorts. Stop at a multiple of ATR. Needs the order book (L2): trades only on bars with a recorded book.
strategy "Book Gravity Reversion" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Fades stretches away from the order book's average centre of gravity (20-bar EMA): when the close is more than the
/// threshold (in ATR) below the size-weighted centre of the top levels and the book still leans to
/// the bids, buy for a return to the centre; mirror for shorts. Stop at a multiple of ATR.
/// Needs the order book (L2): trades only on bars with a recorded book.
levels = param.int(10, title = "Levels per side", min = 1, max = 200)
dist = param.float(1.0, title = "Stretch (ATR)", min = 0.1, max = 10, step = 0.1)
stopk = param.float(1.5, title = "Stop (ATR)", min = 0.2, max = 10, step = 0.1)
bv = book.vwap(book.bid_side, levels)
av = book.vwap(book.ask_side, levels)
bq = book.bid_volume(levels)
aq = book.ask_volume(levels)
g_raw = (bv * bq + av * aq) / math.max(bq + aq, 1e-12)
// the gravity of one bar sits at the price; its average over recent bars is the level price returns to
g = ta.ema(g_raw, 20)
a = ta.atr(14)
imb = book.imbalance(levels)
flat = position.size == 0
long_sig = not math.is_na(a) and not math.is_na(bv) and close < g - dist * a and imb > 0
short_sig = not math.is_na(a) and not math.is_na(bv) and close > g + dist * a and imb < 0
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stopk * a, target = g)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stopk * a, target = g)
}
risk.max_drawdown(25)
render.line(g, "Centre of gravity", color = color.violet.300, width = 2)Book Imbalance Breakout
strategy · presets/strategy/book_imbalance_breakout.qs · uses viz/premium
Range breakouts that the order book agrees with. A Donchian breakout from OHLCV is taken only if the resting book leans the same way at the breakout bar (distance-weighted pressure beyond the threshold) and order-flow imbalance between the last books confirms it, so breakouts into a wall of opposing orders are skipped. Bars without a recorded book never trade (no guessing). Exit: ATR stop and a trailing exit at the opposite Donchian half-channel. Needs the order book (L2): backtests run only where the chart has recorded books.
strategy "Book Imbalance Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Range breakouts that the order book agrees with. A Donchian breakout from OHLCV is taken only if
/// the resting book leans the same way at the breakout bar (distance-weighted pressure beyond the
/// threshold) and order-flow imbalance between the last books confirms it, so breakouts into a wall
/// of opposing orders are skipped. Bars without a recorded book never trade (no guessing).
/// Exit: ATR stop and a trailing exit at the opposite Donchian half-channel.
/// Needs the order book (L2): backtests run only where the chart has recorded books.
use "viz/premium" as pr
len = param.int(30, title = "Breakout channel", min = 5, max = 500, group = "Breakout")
thr = param.float(0.15, title = "Book pressure threshold", min = 0.01, max = 0.9, step = 0.01, group = "Book")
decay = param.float(0.25, title = "Pressure decay (%)", min = 0.02, max = 2, step = 0.01, group = "Book")
stop_k = param.float(2.0, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1, group = "Exit")
(mid, upper, lower) = ta.donchian(len)
(_, up_half, dn_half) = ta.donchian(len / 2)
pres = book.pressure(decay)
ofi = orderbook.ofi(5)
a = ta.atr(14)
break_up = close > upper[1]
break_dn = close < lower[1]
long_ok = break_up and pres > thr and math.coalesce(ofi, 0) >= 0
short_ok = break_dn and pres < -thr and math.coalesce(ofi, 0) <= 0
flat = position.size == 0
if flat and long_ok {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stop_k * a)
}
if flat and short_ok {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stop_k * a)
}
if position.size > 0 and close < dn_half[1] { exit.close_all() }
if position.size < 0 and close > up_half[1] { exit.close_all() }
risk.max_drawdown(25)
pu = render.line(upper, "Upper", color = color.translucent(color.emerald.300, 40), width = 1)
pd = render.line(lower, "Lower", color = color.translucent(color.rose.300, 40), width = 1)
render.band(pu, pd, color = pres > thr ? color.emerald.500 : pres < -thr ? color.rose.500 : color.slate.600, opacity = 0.06, title = "Range (book lean)")
render.mark(break_up and not long_ok, shape = shape.x, at = anchor.above_bar, color = color.amber.300, size = 0.5, title = "Breakout rejected by book")
render.mark(break_dn and not short_ok, shape = shape.x, at = anchor.below_bar, color = color.amber.300, size = 0.5, title = "Breakdown rejected by book")
render.mark(long_ok and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, size = 0.8, title = "Book-confirmed breakout")
render.mark(short_ok and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.8, title = "Book-confirmed breakdown")Book RSI Reversal
strategy · presets/strategy/book_rsi_reversal.qs
Buys when price RSI is oversold while the order book RSI shows bids building (the book disagrees with the selling), sells the mirror. Exit at the RSI midline or an ATR stop. Needs the order book (L2): trades only on bars with a recorded book.
strategy "Book RSI Reversal" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Buys when price RSI is oversold while the order book RSI shows bids building (the book disagrees
/// with the selling), sells the mirror. Exit at the RSI midline or an ATR stop.
/// Needs the order book (L2): trades only on bars with a recorded book.
pct = param.float(0.25, title = "Band around mid (%)", min = 0.02, max = 5, step = 0.01)
stopk = param.float(1.5, title = "Stop (ATR)", min = 0.2, max = 10, step = 0.1)
b = book.liquidity(pct, book.bid_side)
a_ = book.liquidity(pct, book.ask_side)
d = math.coalesce((b - b[1]) - (a_ - a_[1]), 0)
brsi = 100 - 100 / (1 + ta.rma(math.max(d, 0), 14) / math.max(ta.rma(math.max(-d, 0), 14), 1e-12))
r = ta.rsi(close, 14)
a = ta.atr(14)
flat = position.size == 0
if flat and r < 30 and brsi > 55 and not math.is_na(a) and not math.is_na(b) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stopk * a)
}
if flat and r > 70 and brsi < 45 and not math.is_na(a) and not math.is_na(b) {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stopk * a)
}
if position.size > 0 and r > 50 { exit.close_all() }
if position.size < 0 and r < 50 { exit.close_all() }
risk.max_drawdown(25)Compression Release
strategy · presets/strategy/compression_release.qs · uses viz/premium
Trades the expansion that follows a ranked volatility compression. Compression is the mean of two percentile ranks (Bollinger width and short-term range against ATR); a release is the first bar the score leaves the compressed zone, and the side comes from the slope of a short regression measured in ATR per bar, so only directional releases are traded. Entry: compression release with thrust beyond the threshold. Exit: stop at the compression range's far side, target reward R, trailing after 1R, time stop. Risk: 1 % per trade.
strategy "Compression Release" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trades the expansion that follows a ranked volatility compression. Compression is the mean of
/// two percentile ranks (Bollinger width and short-term range against ATR); a release is the first
/// bar the score leaves the compressed zone, and the side comes from the slope of a short
/// regression measured in ATR per bar, so only directional releases are traded.
/// Entry: compression release with thrust beyond the threshold. Exit: stop at the compression
/// range's far side, target `reward` R, trailing after 1R, time stop.
/// Risk: 1 % per trade.
use "viz/premium" as pr
len = param.int(20, title = "Width length", min = 5, max = 200, group = "Compression")
rank_len = param.int(250, title = "Rank window", min = 50, max = 2000, group = "Compression")
tight = param.float(80, title = "Compressed above", min = 50, max = 99, group = "Compression")
th_len = param.int(12, title = "Thrust length", min = 3, max = 100, group = "Thrust")
th_min = param.float(0.08, title = "Release thrust (ATR / bar)", min = 0, max = 1, step = 0.01, group = "Thrust")
reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
hold = param.int(60, title = "Maximum bars held", min = 2, max = 1000, group = "Risk")
bbw = ta.bollinger_width(close, len, 2)
range_ratio = ta.sma(ta.true_range(), 5) / math.max(ta.atr(len * 5), 1e-12)
score = 100 - (stat.percent_rank(bbw, rank_len) + stat.percent_rank(range_ratio, rank_len)) / 2
(_, slope, _) = stat.linreg(close, th_len, 0)
a = ta.atr(len)
thrust = slope / math.max(a, 1e-12)
compressed = score >= tight
box_hi = ta.highest(high, len)
box_lo = ta.lowest(low, len)
released = compressed[1] and not compressed
flat = position.size == 0
if flat and released and thrust > th_min {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = box_lo, target = close + (close - box_lo) * reward, trail_offset = close - box_lo, trail_trigger = close + (close - box_lo))
}
if flat and released and thrust < -th_min {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = box_hi, target = close - (box_hi - close) * reward, trail_offset = box_hi - close, trail_trigger = close - (box_hi - close))
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
ph = render.line(box_hi, "Range high", color = color.translucent(color.violet.300, compressed ? 20 : 70), width = 1)
plo = render.line(box_lo, "Range low", color = color.translucent(color.violet.300, compressed ? 20 : 70), width = 1)
render.band(ph, plo, color = color.translucent(color.violet.500, compressed ? 82 : 98), opacity = 1, title = "Compression box")
render.mark(released and thrust > th_min, shape = shape.arrow_up, at = anchor.below_bar, color = color.cyan.300, text = "Release", title = "Release up")
render.mark(released and thrust < -th_min, shape = shape.arrow_down, at = anchor.above_bar, color = color.pink.300, text = "Release", title = "Release down")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Compression Release", "Strategy · ranked squeeze, regression thrust")
pr.row(t, 2, "Compression score", str.format("{:.0f}", score), compressed ? color.violet.300 : color.white)
pr.row(t, 3, "Thrust", str.format("{:+.3f} ATR / bar", thrust), thrust > 0 ? pr.BULL : pr.BEAR)
}Cornish-Fisher Tail Fade
strategy · presets/strategy/cornish_fisher_tail_fade.qs · uses viz/premium
Fades moves that reach the fat-tail-adjusted 99 % quantile and close back inside it. The quantiles come from a Cornish-Fisher expansion of the rolling return distribution (skew and excess kurtosis), so the bands widen when tails are heavy and the fade only triggers on moves that are extreme even for this market. Entry: the bar trades through the 99 % band and closes back inside the 95 % band (rejection). Exit: target at the previous close (the distribution centre), stop beyond the bar's extreme, time stop after hold bars. Risk: 1 % per trade. Method: Cornish and Fisher (1938).
strategy "Cornish-Fisher Tail Fade" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Fades moves that reach the fat-tail-adjusted 99 % quantile and close back inside it. The
/// quantiles come from a Cornish-Fisher expansion of the rolling return distribution (skew and
/// excess kurtosis), so the bands widen when tails are heavy and the fade only triggers on moves
/// that are extreme even for this market.
/// Entry: the bar trades through the 99 % band and closes back inside the 95 % band (rejection).
/// Exit: target at the previous close (the distribution centre), stop beyond the bar's extreme,
/// time stop after `hold` bars. Risk: 1 % per trade. Method: Cornish and Fisher (1938).
use "viz/premium" as pr
len = param.int(250, title = "Return window", min = 30, max = 3000, group = "Distribution")
hold = param.int(12, title = "Maximum bars held", min = 1, max = 200, group = "Exit")
r = math.log(close / close[1])
mu = ta.sma(r, len)
sd = stat.stdev(r, len)
S = math.clamp(stat.skew(r, len), -3, 3)
K = math.clamp(stat.kurtosis(r, len), -2, 20)
fn cf(z: float, s: float, k: float) -> float {
return z + (z * z - 1) * s / 6 + (z * z * z - 3 * z) * k / 24 - (2 * z * z * z - 5 * z) * s * s / 36
}
zu99 = cf(2.326, S, K)
zd99 = cf(-2.326, S, K)
zu95 = cf(1.645, S, K)
zd95 = cf(-1.645, S, K)
u99 = close[1] * math.exp(mu[1] + zu99[1] * sd[1])
d99 = close[1] * math.exp(mu[1] + zd99[1] * sd[1])
u95 = close[1] * math.exp(mu[1] + zu95[1] * sd[1])
d95 = close[1] * math.exp(mu[1] + zd95[1] * sd[1])
flat = position.size == 0
rej_lo = low < d99 and close > d95
rej_hi = high > u99 and close < u95
if flat and rej_lo {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = low - 0.25 * (high - low), target = close + (close[1] - low) * 0.8)
}
if flat and rej_hi {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = high + 0.25 * (high - low), target = close - (high - close[1]) * 0.8)
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
p1 = render.line(u99, "Upper 99 %", color = color.translucent(color.orange.300, 40), width = 1)
p2 = render.line(u95, "Upper 95 %", color = color.transparent, width = 0)
p3 = render.line(d95, "Lower 95 %", color = color.transparent, width = 0)
p4 = render.line(d99, "Lower 99 %", color = color.translucent(color.sky.300, 40), width = 1)
render.band(p1, p2, upper_color = color.ramp(K[1], 0, 8, "inferno"), lower_color = color.transparent, opacity = 0.5, title = "Upper tail")
render.band(p3, p4, upper_color = color.transparent, lower_color = color.ramp(K[1], 0, 8, "inferno"), opacity = 0.5, title = "Lower tail")
render.mark(rej_lo, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, size = 0.7, title = "Lower tail rejection")
render.mark(rej_hi, shape = shape.triangle_down, at = anchor.above_bar, color = color.orange.300, size = 0.7, title = "Upper tail rejection")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Cornish-Fisher Tail Fade", "Strategy · modified VaR rejection")
pr.row(t, 2, "Excess kurtosis", str.format("{:.2f}", K), K > 3 ? pr.WARN : color.white)
pr.row(t, 3, "Skewness", str.format("{:+.2f}", S), color.white)
}Curvature Turn
strategy · presets/strategy/curvature_turn.qs · uses viz/premium
Enters on the earliest measurable sign of a turn: the curvature of a quadratic least-squares fit changing sign. When a downtrend starts to decelerate, the quadratic term turns positive before the slope does; the strategy buys once the slope also turns up, and sells the mirror case. It exits when the curvature flips back. Entry: curvature positive for confirm bars and the fitted slope crossing above zero (mirror for shorts). Exit: curvature flips against the trade, ATR stop, or a target of reward R. Risk: 1 % per trade. Method: ordinary least squares on 1, x, x squared.
strategy "Curvature Turn" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Enters on the earliest measurable sign of a turn: the curvature of a quadratic least-squares fit
/// changing sign. When a downtrend starts to decelerate, the quadratic term turns positive before
/// the slope does; the strategy buys once the slope also turns up, and sells the mirror case. It
/// exits when the curvature flips back.
/// Entry: curvature positive for `confirm` bars and the fitted slope crossing above zero (mirror for
/// shorts). Exit: curvature flips against the trade, ATR stop, or a target of `reward` R.
/// Risk: 1 % per trade. Method: ordinary least squares on 1, x, x squared.
use "viz/premium" as pr
len = param.int(80, title = "Fit window", min = 20, max = 500, group = "Fit")
confirm = param.int(3, title = "Curvature confirmation (bars)", min = 1, max = 20, group = "Entry")
stop_k = param.float(2.5, title = "Stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
reward = param.float(3, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
s0 = 0.0
s1 = 0.0
s2 = 0.0
s3 = 0.0
s4 = 0.0
t0 = 0.0
t1 = 0.0
t2 = 0.0
for i in 0..len {
y = close[i]
if not math.is_na(y) {
x = -i
s0 := s0 + 1
s1 := s1 + x
s2 := s2 + x * x
s3 := s3 + x * x * x
s4 := s4 + x * x * x * x
t0 := t0 + y
t1 := t1 + x * y
t2 := t2 + x * x * y
}
}
det = s0 * (s2 * s4 - s3 * s3) - s1 * (s1 * s4 - s3 * s2) + s2 * (s1 * s3 - s2 * s2)
c0 = det != 0 ? (t0 * (s2 * s4 - s3 * s3) - s1 * (t1 * s4 - s3 * t2) + s2 * (t1 * s3 - s2 * t2)) / det : na
c1 = det != 0 ? (s0 * (t1 * s4 - t2 * s3) - t0 * (s1 * s4 - s3 * s2) + s2 * (s1 * t2 - t1 * s2)) / det : na
c2 = det != 0 ? (s0 * (s2 * t2 - s3 * t1) - s1 * (s1 * t2 - s2 * t1) + t0 * (s1 * s3 - s2 * s2)) / det : na
a = ta.atr(20)
pos_c = ta.bars_since(c2 <= 0)
neg_c = ta.bars_since(c2 >= 0)
long_sig = pos_c >= confirm and c1 > 0 and c1[1] <= 0
short_sig = neg_c >= confirm and c1 < 0 and c1[1] >= 0
flat = position.size == 0
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = close + stop_k * a * reward)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = close - stop_k * a * reward)
}
if (position.side > 0 and c2 < 0) or (position.side < 0 and c2 > 0) { exit.close_all() }
risk.max_drawdown(25)
render.line(c0, "Fit end", color = c2 > 0 ? color.sky.300 : color.amber.300, width = 2)
render.mark(long_sig, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, text = "Turn", title = "Turn up")
render.mark(short_sig, shape = shape.triangle_down, at = anchor.above_bar, color = color.amber.300, text = "Turn", title = "Turn down")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Curvature Turn", "Strategy · quadratic fit turns")
pr.row(t, 2, "Slope", str.format("{:+.3f}", c1), c1 > 0 ? pr.BULL : pr.BEAR)
pr.row(t, 3, "Curvature", c2 > 0 ? "Positive" : "Negative", c2 > 0 ? color.sky.300 : color.amber.300)
}Drawdown Recovery
strategy · presets/strategy/drawdown_recovery.qs · uses viz/premium
Buys measured pullbacks inside a long-term uptrend and only once the pullback shows it is over. The drawdown from the rolling high is measured in ATR units; a pullback of at least the minimum depth, with price still above the long-term average, is bought when price reclaims the prior bar's high. The trade aims for a retest of the high that started the pullback. Entry: drawdown between the minimum and maximum depth, close above the slow EMA, close above the previous high. Exit: target at the pullback's starting high, stop below the pullback low, time stop. Long only. Risk: 1 % per trade.
strategy "Drawdown Recovery" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Buys measured pullbacks inside a long-term uptrend and only once the pullback shows it is over.
/// The drawdown from the rolling high is measured in ATR units; a pullback of at least the minimum
/// depth, with price still above the long-term average, is bought when price reclaims the prior
/// bar's high. The trade aims for a retest of the high that started the pullback.
/// Entry: drawdown between the minimum and maximum depth, close above the slow EMA, close above
/// the previous high. Exit: target at the pullback's starting high, stop below the pullback low,
/// time stop. Long only. Risk: 1 % per trade.
use "viz/premium" as pr
look = param.int(60, title = "High lookback", min = 10, max = 1000, group = "Pullback")
d_min = param.float(3, title = "Minimum depth (ATR)", min = 0.5, max = 20, step = 0.5, group = "Pullback")
d_max = param.float(10, title = "Maximum depth (ATR)", min = 1, max = 40, step = 0.5, group = "Pullback")
trend = param.int(200, title = "Trend EMA", min = 20, max = 2000, group = "Trend")
hold = param.int(80, title = "Maximum bars held", min = 2, max = 1000, group = "Exit")
hh = ta.highest(high, look)
ll = ta.lowest(low, 10)
a = math.max(ta.atr(50), 1e-12)
depth = (hh - close) / a
t = ta.ema(close, trend)
setup = depth[1] >= d_min and depth[1] <= d_max and close > t and close > high[1]
if position.size == 0 and setup {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = ll - 0.2 * a, target = hh)
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
render.line(hh, "Rolling high", color = color.translucent(color.emerald.300, 40), width = 1, stroke = style.dashed)
render.line(t, "Trend EMA", color = color.amber.300, width = 1.5)
render.mark(setup and position.size == 0, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Recover", title = "Pullback recovery")
persist tb = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(tb, 2, "Drawdown Recovery", "Strategy · measured pullbacks")
pr.row(tb, 2, "Depth", str.format("{:.1f} ATR", depth), depth >= d_min ? pr.WARN : color.white)
pr.row(tb, 3, "Trend", close > t ? "Up" : "Down", close > t ? pr.BULL : pr.BEAR)
}Efficiency Trend
strategy · presets/strategy/efficiency_trend.qs
Joins trends when price moves efficiently on several horizons at once (Kaufman efficiency, signed by direction, above the threshold on most of eight horizons) and exits when the move turns noisy again or a Kaufman-average trailing stop is hit.
strategy "Efficiency Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Joins trends when price moves efficiently on several horizons at once (Kaufman efficiency,
/// signed by direction, above the threshold on most of eight horizons) and exits when the move
/// turns noisy again or a Kaufman-average trailing stop is hit.
thr = param.float(0.2, title = "Efficiency threshold", min = 0.05, max = 0.95, step = 0.01)
need = param.int(5, title = "Horizons required (of 8)", min = 1, max = 8)
stopk = param.float(2.5, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1)
up_n = 0
dn_n = 0
for len in [8, 13, 21, 34, 55, 89, 144, 233] {
q = hft.efficiency(close, len) * math.sign(close - close[len])
if q > thr { up_n := up_n + 1 }
if q < -thr { dn_n := dn_n + 1 }
}
kama = ta.kaufman(close, 21, 2, 30)
a = ta.atr(14)
flat = position.size == 0
if flat and up_n >= need and not math.is_na(a) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stopk * a, trail_offset = stopk * a, trail_trigger = close + a)
}
if flat and dn_n >= need and not math.is_na(a) {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stopk * a, trail_offset = stopk * a, trail_trigger = close - a)
}
if position.size > 0 and (up_n < need / 2 or close < kama) { exit.close_all() }
if position.size < 0 and (dn_n < need / 2 or close > kama) { exit.close_all() }
risk.max_drawdown(25)
render.line(kama, "Kaufman average", color = up_n >= need ? color.lime.300 : dn_n >= need ? color.fuchsia.400 : color.slate.400, width = 2)
render.candle_color(up_n >= need ? color.lime.500 : dn_n >= need ? color.fuchsia.500 : na)Entropy Order Breakout
strategy · presets/strategy/entropy_order_breakout.qs · uses viz/premium
Joins a trend at the moment the market becomes ordered. The Shannon entropy of recent returns falls when moves start to repeat; the first bar it drops below the order level, the strategy enters in the direction of an adaptive average's slope, and it rides the move with a trailing stop until the market turns random again. Entry: entropy crosses below the order level, adaptive mean sloping, close on the right side of it. Exit: ATR trailing stop, or entropy back above the disorder level. Risk: 1 % per trade. Method: Shannon (1948) entropy of the return histogram.
strategy "Entropy Order Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Joins a trend at the moment the market becomes ordered. The Shannon entropy of recent returns
/// falls when moves start to repeat; the first bar it drops below the order level, the strategy
/// enters in the direction of an adaptive average's slope, and it rides the move with a trailing
/// stop until the market turns random again.
/// Entry: entropy crosses below the order level, adaptive mean sloping, close on the right side of
/// it. Exit: ATR trailing stop, or entropy back above the disorder level.
/// Risk: 1 % per trade. Method: Shannon (1948) entropy of the return histogram.
use "viz/premium" as pr
len = param.int(60, title = "Entropy window", min = 20, max = 1000, group = "Entropy")
bins = param.int(10, title = "Histogram bins", min = 4, max = 40, group = "Entropy")
order_lv = param.float(0.86, title = "Order level", min = 0.3, max = 0.99, step = 0.01, group = "Entropy")
chaos_lv = param.float(0.93, title = "Disorder level", min = 0.4, max = 1, step = 0.01, group = "Entropy")
mean_l = param.int(34, title = "Adaptive mean", min = 5, max = 400, group = "Trend")
stop_k = param.float(2.2, title = "Trailing stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
e = stat.entropy(math.log(close / close[1]), len, bins)
c = ta.kaufman(close, mean_l)
a = ta.atr(20)
up = c > c[1]
ordered = e < order_lv and e[1] >= order_lv
flat = position.size == 0
if flat and ordered and up and close > c {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, trail_offset = stop_k * a, trail_trigger = close)
}
if flat and ordered and not up and close < c {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, trail_offset = stop_k * a, trail_trigger = close)
}
if position.size != 0 and e > chaos_lv { exit.close_all() }
risk.max_drawdown(25)
render.line(c, "Adaptive mean", color = e < order_lv ? (up ? color.cyan.300 : color.fuchsia.300) : color.slate.500, width = 2)
render.mark(ordered, shape = shape.diamond, at = anchor.above_bar, color = color.amber.300, size = 0.5, title = "Market turns ordered")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Entropy Order Breakout", "Strategy · Shannon entropy")
pr.row(t, 2, "Entropy", str.format("{:.3f}", e), e < order_lv ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Market", e < order_lv ? "Ordered" : e > chaos_lv ? "Random" : "Mixed", color.white)
}Fractal Trend Rider
strategy · presets/strategy/fractal_trend_rider.qs · uses viz/premium
Trades moving-average crosses only when the price path is smooth enough to trend. The Sevcik fractal dimension of price is low (near 1) for a clean trend and high (near 2) for chop; crosses in choppy conditions, where most whipsaws come from, are skipped. Entry: fast EMA crosses the slow EMA while the fractal dimension is below the trend level. Exit: opposite cross, ATR trailing stop, or the path turning choppy. Risk: 1 % per trade. Method: Sevcik (1998) fractal dimension.
strategy "Fractal Trend Rider" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trades moving-average crosses only when the price path is smooth enough to trend. The Sevcik
/// fractal dimension of price is low (near 1) for a clean trend and high (near 2) for chop; crosses
/// in choppy conditions, where most whipsaws come from, are skipped.
/// Entry: fast EMA crosses the slow EMA while the fractal dimension is below the trend level.
/// Exit: opposite cross, ATR trailing stop, or the path turning choppy.
/// Risk: 1 % per trade. Method: Sevcik (1998) fractal dimension.
use "viz/premium" as pr
fast_l = param.int(21, title = "Fast EMA", min = 2, max = 200, group = "Trend")
slow_l = param.int(55, title = "Slow EMA", min = 5, max = 500, group = "Trend")
fd_len = param.int(40, title = "Fractal window", min = 10, max = 500, group = "Filter")
fd_max = param.float(1.38, title = "Trend level", min = 1.05, max = 1.9, step = 0.01, group = "Filter")
stop_k = param.float(2.5, title = "Trailing stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
f = ta.ema(close, fast_l)
s = ta.ema(close, slow_l)
fd = stat.fractal_dim(close, fd_len)
a = ta.atr(20)
smooth = fd < fd_max
up_x = ta.cross_up(f, s)
dn_x = ta.cross_down(f, s)
if up_x and smooth {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, trail_offset = stop_k * a, trail_trigger = close)
}
if dn_x and smooth {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, trail_offset = stop_k * a, trail_trigger = close)
}
if (position.side > 0 and dn_x) or (position.side < 0 and up_x) { exit.close_all() }
risk.max_drawdown(25)
pf = render.line(f, "Fast EMA", color = smooth ? color.sky.300 : color.slate.500, width = 2)
ps = render.line(s, "Slow EMA", color = color.translucent(color.slate.300, 30), width = 1.5)
render.band(pf, ps, color = color.translucent(f > s ? color.sky.500 : color.rose.500, smooth ? 84 : 96), opacity = 1, title = "Trend ribbon")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Fractal Trend Rider", "Strategy · Sevcik dimension filter")
pr.row(t, 2, "Fractal dimension", str.format("{:.3f}", fd), smooth ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Crosses", smooth ? "Taken" : "Skipped (chop)", color.white)
}GARCH Sigma Breakout
strategy · presets/strategy/garch_sigma_breakout.qs · uses viz/premium
Buys or sells genuine volatility events. A GARCH(1,1) forecast gives the expected one-bar move; a close beyond k forecast sigmas from the previous close, in the direction of the longer trend, is an event the model did not expect, and such events tend to start moves rather than end them. Entry: |close - previous close| above k GARCH sigmas, with the close on the trend side of the slow EMA. Exit: stop at the event bar's far side, target reward R, trailing after 1R. Risk: 1 % per trade. Method: Bollerslev (1986).
strategy "GARCH Sigma Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Buys or sells genuine volatility events. A GARCH(1,1) forecast gives the expected one-bar move;
/// a close beyond k forecast sigmas from the previous close, in the direction of the longer trend,
/// is an event the model did not expect, and such events tend to start moves rather than end them.
/// Entry: |close - previous close| above k GARCH sigmas, with the close on the trend side of the
/// slow EMA. Exit: stop at the event bar's far side, target `reward` R, trailing after 1R.
/// Risk: 1 % per trade. Method: Bollerslev (1986).
use "viz/premium" as pr
alpha = param.float(0.06, title = "Alpha (shock weight)", min = 0.01, max = 0.3, step = 0.01, group = "GARCH")
beta = param.float(0.92, title = "Beta (persistence)", min = 0.5, max = 0.99, step = 0.01, group = "GARCH")
k = param.float(2.5, title = "Event size (sigma)", min = 1, max = 6, step = 0.1, group = "Entry")
trend = param.int(200, title = "Trend EMA", min = 20, max = 1000, group = "Entry")
reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
(vol, _) = hft.garch(close, alpha, beta, 0)
sig = vol / 100
s_prev = sig[1]
move = (close - close[1]) / math.max(close[1] * s_prev, 1e-12)
t = ta.ema(close, trend)
flat = position.size == 0
up_ev = move > k and close > t
dn_ev = move < -k and close < t
if flat and up_ev {
stop = low
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = stop, target = close + (close - stop) * reward, trail_offset = close - stop, trail_trigger = close + (close - stop))
}
if flat and dn_ev {
stop = high
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = stop, target = close - (stop - close) * reward, trail_offset = stop - close, trail_trigger = close - (stop - close))
}
risk.max_drawdown(25)
pu = render.line(close[1] * (1 + k * s_prev), "Event high", color = color.translucent(color.orange.300, 50), width = 1)
pl = render.line(close[1] * (1 - k * s_prev), "Event low", color = color.translucent(color.sky.300, 50), width = 1)
render.band(pu, pl, color = color.indigo.400, opacity = 0.05, title = "Expected range")
render.line(t, "Trend EMA", color = color.amber.300, width = 1.5)
render.mark(up_ev, shape = shape.diamond, at = anchor.below_bar, color = color.orange.300, size = 0.6, title = "Up event")
render.mark(dn_ev, shape = shape.diamond, at = anchor.above_bar, color = color.sky.300, size = 0.6, title = "Down event")
persist tb = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(tb, 2, "GARCH Sigma Breakout", "Strategy · volatility events")
pr.row(tb, 2, "Forecast sigma", str.format("{:.2%}", sig), color.white)
pr.row(tb, 3, "Last move", str.format("{:+.1f} sigma", move), math.abs(move) > k ? pr.WARN : color.white)
}Golden Pocket Continuation
strategy · presets/strategy/golden_pocket_continuation.qs · uses viz/premium
Joins an impulse leg on its first deep retracement. The newest leg between two confirmed swings must be an impulse (at least impulse ATR long). When price then retraces into the 50 - 61.8 % band of that leg (the "golden pocket") and prints a bar that closes back in the leg's direction above the prior bar's high, the pullback is treated as finished. The stop sits beyond the 78.6 % level, where the leg's structure would be broken, and the first target is a retest of the leg's end with the remainder aimed at the 127.2 % extension. Entry: up-leg of impulse ATR or more, low inside the pocket, bullish close above the prior high (mirror for down-legs). Exit: stop beyond the 78.6 % level, half at the leg end, the rest at the 127.2 % extension; setup cancelled once price passes 78.6 %. Risk: 1 % per trade.
strategy "Golden Pocket Continuation" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Joins an impulse leg on its first deep retracement. The newest leg between two confirmed swings
/// must be an impulse (at least `impulse` ATR long). When price then retraces into the 50 - 61.8 %
/// band of that leg (the "golden pocket") and prints a bar that closes back in the leg's direction
/// above the prior bar's high, the pullback is treated as finished. The stop sits beyond the 78.6 %
/// level, where the leg's structure would be broken, and the first target is a retest of the leg's
/// end with the remainder aimed at the 127.2 % extension.
/// Entry: up-leg of `impulse` ATR or more, low inside the pocket, bullish close above the prior
/// high (mirror for down-legs). Exit: stop beyond the 78.6 % level, half at the leg end, the rest at
/// the 127.2 % extension; setup cancelled once price passes 78.6 %. Risk: 1 % per trade.
use "viz/premium" as pr
left = param.int(10, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right = param.int(5, title = "Swing bars right", min = 1, max = 30, group = "Swings")
impulse = param.float(4.0, title = "Minimum leg (ATR)", min = 1, max = 20, step = 0.5, group = "Swings")
ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
a = ta.atr(50)
persist hi_p = na
persist lo_p = na
persist hi_x = 0
persist lo_x = 0
persist leg = 0
if not math.is_na(ph) {
hi_p := ph
hi_x := bar.index - right
leg := hi_x > lo_x and not math.is_na(lo_p) ? 1 : leg
}
if not math.is_na(pl) {
lo_p := pl
lo_x := bar.index - right
leg := lo_x > hi_x and not math.is_na(hi_p) ? -1 : leg
}
span = hi_p - lo_p
strong = span >= impulse * a
// up-leg retracement levels measured down from the high; down-leg levels measured up from the low
p50 = leg == 1 ? hi_p - span * 0.5 : lo_p + span * 0.5
p618 = leg == 1 ? hi_p - span * 0.618 : lo_p + span * 0.618
p786 = leg == 1 ? hi_p - span * 0.786 : lo_p + span * 0.786
ext = leg == 1 ? hi_p + span * 0.272 : lo_p - span * 0.272
persist armed = false
if (not math.is_na(ph) or not math.is_na(pl)) { armed := strong and leg != 0 }
if leg == 1 and low < p786 { armed := false }
if leg == -1 and high > p786 { armed := false }
go_l = armed and leg == 1 and low <= p50 and low >= p786 and close > open and close > high[1] and close < hi_p
go_s = armed and leg == -1 and high >= p50 and high <= p786 and close < open and close < low[1] and close > lo_p
flat = position.size == 0
if flat and go_l {
stop = p786 - 0.25 * a
order.market("L", side.long)
exit.bracket("L half", entry = "L", stop = stop, target = hi_p, qty_pct = 50)
exit.bracket("L rest", entry = "L", stop = stop, target = ext)
armed := false
}
if flat and go_s {
stop = p786 + 0.25 * a
order.market("S", side.short)
exit.bracket("S half", entry = "S", stop = stop, target = lo_p, qty_pct = 50)
exit.bracket("S rest", entry = "S", stop = stop, target = ext)
armed := false
}
risk.max_drawdown(25)
show = armed and strong
z_top = render.line(show ? p50 : na, "Pocket 50 %", color = color.translucent(color.amber.300, 30), width = 1)
z_bot = render.line(show ? p618 : na, "Pocket 61.8 %", color = color.translucent(color.amber.300, 30), width = 1)
render.band(z_top, z_bot, texture = "gradient", upper_color = color.translucent(color.amber.300, 70), lower_color = color.translucent(color.orange.500, 70), opacity = 1, title = "Golden pocket")
render.line(show ? p786 : na, "Invalidation 78.6 %", color = color.translucent(color.rose.400, 30), width = 1, stroke = style.dotted)
render.line(show ? ext : na, "Extension 127.2 %", color = color.translucent(color.emerald.300, 40), width = 1, stroke = style.dashed)
render.mark(go_l and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.amber.300, text = "GP", title = "Long from pocket")
render.mark(go_s and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.orange.400, text = "GP", title = "Short from pocket")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Golden Pocket Continuation", "Strategy · impulse retracement")
pr.row(t, 2, "Leg", leg == 1 ? str.format("Up {:.1f} ATR", span / a) : leg == -1 ? str.format("Down {:.1f} ATR", span / a) : "None", leg == 1 ? pr.BULL : leg == -1 ? pr.BEAR : pr.LABEL)
pr.row(t, 3, "Pocket", show ? str.format("{:.2f} .. {:.2f}", math.min(p50, p618), math.max(p50, p618)) : "Not armed", color.amber.300)
pr.row(t, 4, "Invalidation", show ? str.format("{:.2f}", p786) : "-", pr.BEAR)
}Hawkes Shock Reversal
strategy · presets/strategy/hawkes_shock_reversal.qs · uses viz/premium
Buys the end of a selling cascade and sells the end of a buying frenzy. Large moves are counted as events in a self-exciting Hawkes process, so clusters build intensity and the intensity then decays. When a cluster of down shocks has decayed (calm returning) and price closes back above a short mean, the forced selling is likely done. Entry: down-shock intensity peaked above the cluster level and has since decayed below the calm level, close above the 10-bar EMA (mirror for shorts). Exit: stop below the cascade low, target reward R, time stop. Risk: 1 % per trade. Method: Hawkes (1971).
strategy "Hawkes Shock Reversal" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Buys the end of a selling cascade and sells the end of a buying frenzy. Large moves are counted
/// as events in a self-exciting Hawkes process, so clusters build intensity and the intensity then
/// decays. When a cluster of down shocks has decayed (calm returning) and price closes back above
/// a short mean, the forced selling is likely done.
/// Entry: down-shock intensity peaked above the cluster level and has since decayed below the calm
/// level, close above the 10-bar EMA (mirror for shorts). Exit: stop below the cascade low, target
/// `reward` R, time stop. Risk: 1 % per trade. Method: Hawkes (1971).
use "viz/premium" as pr
len = param.int(100, title = "Volatility window", min = 20, max = 2000, group = "Shocks")
k_sd = param.float(2.0, title = "Shock size (sigma)", min = 1, max = 5, step = 0.1, group = "Shocks")
decay = param.float(0.12, title = "Decay per bar", min = 0.01, max = 0.8, step = 0.01, group = "Process")
hot = param.float(1.2, title = "Cluster level", min = 0.2, max = 10, step = 0.1, group = "Signals")
calm = param.float(0.3, title = "Calm level", min = 0.01, max = 5, step = 0.01, group = "Signals")
reward = param.float(2.0, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
hold = param.int(48, title = "Maximum bars held", min = 2, max = 500, group = "Risk")
r = math.log(close / close[1])
sd_now = stat.stdev(r, len)
sd = sd_now[1]
lam_d = hft.hawkes(r < -k_sd * sd, decay, 1)
lam_u = hft.hawkes(r > k_sd * sd, decay, 1)
e10 = ta.ema(close, 10)
low20 = ta.lowest(low, 20)
high20 = ta.highest(high, 20)
persist armed_l = false
persist armed_s = false
if lam_d > hot { armed_l := true }
if lam_u > hot { armed_s := true }
flat = position.size == 0
go_l = armed_l and lam_d < calm and close > e10
go_s = armed_s and lam_u < calm and close < e10
if flat and go_l {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = low20, target = close + (close - low20) * reward)
armed_l := false
}
if flat and go_s {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = high20, target = close - (high20 - close) * reward)
armed_s := false
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
render.line(e10, "10-bar EMA", color = color.slate.300, width = 1)
render.mark(lam_d > hot and lam_d[1] <= hot, shape = shape.x, at = anchor.below_bar, color = color.rose.400, size = 0.6, title = "Selling cascade")
render.mark(lam_u > hot and lam_u[1] <= hot, shape = shape.x, at = anchor.above_bar, color = color.emerald.400, size = 0.6, title = "Buying frenzy")
render.mark(go_l and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, text = "Calm", title = "Long after cascade")
render.mark(go_s and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.amber.300, text = "Calm", title = "Short after frenzy")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Hawkes Shock Reversal", "Strategy · self-exciting shocks")
pr.row(t, 2, "Down / up intensity", str.format("{:.2f} / {:.2f}", lam_d, lam_u), color.white)
pr.row(t, 3, "Armed", armed_l ? "Long" : armed_s ? "Short" : "No", armed_l ? pr.BULL : armed_s ? pr.BEAR : pr.LABEL)
}Hurst Regime Switch
strategy · presets/strategy/hurst_regime_switch.qs · uses viz/premium
Lets the Hurst exponent pick the playbook. When returns persist (H above the upper band) it trades breakouts of a Donchian channel in the direction of the move; when they mean-revert (H below the lower band) it fades closes outside a 2-sigma band back to the mean; in the random-walk zone between it stands aside. Entry: trend mode, close beyond the channel; reversion mode, close outside the band with the band turning back. Exit: ATR bracket (trend: trailing; reversion: target at the mean), or a regime change. Risk: 1 % of equity per trade, 25 % drawdown halt. Method: Hurst (1951) rescaled range on log returns.
strategy "Hurst Regime Switch" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Lets the Hurst exponent pick the playbook. When returns persist (H above the upper band) it
/// trades breakouts of a Donchian channel in the direction of the move; when they mean-revert
/// (H below the lower band) it fades closes outside a 2-sigma band back to the mean; in the
/// random-walk zone between it stands aside.
/// Entry: trend mode, close beyond the channel; reversion mode, close outside the band with the
/// band turning back. Exit: ATR bracket (trend: trailing; reversion: target at the mean), or a
/// regime change. Risk: 1 % of equity per trade, 25 % drawdown halt.
/// Method: Hurst (1951) rescaled range on log returns.
use "viz/premium" as pr
h_len = param.int(200, title = "Hurst window", min = 50, max = 2000, group = "Regime")
band = param.float(0.04, title = "Random-walk band", min = 0.0, max = 0.2, step = 0.01, group = "Regime")
ch_len = param.int(30, title = "Breakout channel", min = 5, max = 300, group = "Trend mode")
mr_len = param.int(40, title = "Reversion band length", min = 10, max = 400, group = "Reversion mode")
stop_k = param.float(2.0, title = "Stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
h = stat.hurst(math.log(close / close[1]), h_len)
trend_mode = h > 0.5 + band
revert_mode = h < 0.5 - band
(_, upper, lower) = ta.donchian(ch_len)
(mid, bb_up, bb_lo) = ta.bollinger(close, mr_len, 2)
a = ta.atr(20)
flat = position.size == 0
if flat and trend_mode and close > upper[1] {
order.market("TL", side.long)
exit.bracket("TL exit", entry = "TL", stop = close - stop_k * a, trail_offset = stop_k * a, trail_trigger = close + stop_k * a)
}
if flat and trend_mode and close < lower[1] {
order.market("TS", side.short)
exit.bracket("TS exit", entry = "TS", stop = close + stop_k * a, trail_offset = stop_k * a, trail_trigger = close - stop_k * a)
}
if flat and revert_mode and close[1] < bb_lo[1] and close > close[1] {
order.market("RL", side.long)
exit.bracket("RL exit", entry = "RL", stop = close - stop_k * a, target = mid)
}
if flat and revert_mode and close[1] > bb_up[1] and close < close[1] {
order.market("RS", side.short)
exit.bracket("RS exit", entry = "RS", stop = close + stop_k * a, target = mid)
}
// a regime change closes the trade it no longer supports
if position.size != 0 and not trend_mode and not revert_mode and position.bars_held > 5 { exit.close_all() }
risk.max_drawdown(25)
render.line(upper, "Channel high", color = color.translucent(color.orange.300, 50), width = 1)
render.line(lower, "Channel low", color = color.translucent(color.sky.300, 50), width = 1)
render.line(mid, "Mean", color = trend_mode ? color.orange.300 : revert_mode ? color.sky.300 : color.slate.500, width = 2)
render.background(trend_mode ? color.translucent(color.orange.500, 94) : revert_mode ? color.translucent(color.sky.500, 94) : na)
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Hurst Regime Switch", "Strategy · regime-selected playbook")
pr.row(t, 2, "Hurst", str.format("{:.3f}", h), trend_mode ? color.orange.300 : revert_mode ? color.sky.300 : color.slate.300)
pr.row(t, 3, "Playbook", trend_mode ? "Breakout" : revert_mode ? "Fade to mean" : "Stand aside", color.white)
pr.row(t, 4, "Position", position.side > 0 ? "Long" : position.side < 0 ? "Short" : "Flat", position.side > 0 ? pr.BULL : position.side < 0 ? pr.BEAR : pr.LABEL)
}Liquidity Impact Breakout
strategy · presets/strategy/liquidity_impact_breakout.qs · uses viz/premium
Takes range breakouts when liquidity is thin. Amihud illiquidity (absolute return per unit of traded value) is ranked against its own history; when it is high, a given order moves price further, so breakouts through a recent range tend to travel instead of being absorbed. Entry: close beyond the N-bar range with the illiquidity rank above the threshold. Exit: stop at the range middle, target reward R, trailing after 1R. Risk: 1 % per trade. Method: Amihud (2002) illiquidity, percentile rank.
strategy "Liquidity Impact Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Takes range breakouts when liquidity is thin. Amihud illiquidity (absolute return per unit of
/// traded value) is ranked against its own history; when it is high, a given order moves price
/// further, so breakouts through a recent range tend to travel instead of being absorbed.
/// Entry: close beyond the N-bar range with the illiquidity rank above the threshold. Exit: stop at
/// the range middle, target `reward` R, trailing after 1R. Risk: 1 % per trade.
/// Method: Amihud (2002) illiquidity, percentile rank.
use "viz/premium" as pr
range_n = param.int(36, title = "Range bars", min = 5, max = 500, group = "Breakout")
ami_len = param.int(24, title = "Illiquidity window", min = 5, max = 500, group = "Liquidity")
rank_n = param.int(500, title = "Rank history", min = 50, max = 5000, group = "Liquidity")
thin = param.float(70, title = "Thin-liquidity rank", min = 30, max = 99, group = "Liquidity")
reward = param.float(2.0, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
hi = ta.highest(high, range_n)
lo = ta.lowest(low, range_n)
mid = (hi + lo) / 2
rank = stat.percent_rank(hft.amihud(ami_len), rank_n)
thin_now = rank > thin
flat = position.size == 0
up_b = thin_now and close > hi[1]
dn_b = thin_now and close < lo[1]
if flat and up_b {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = mid[1], target = close + (close - mid[1]) * reward, trail_offset = close - mid[1], trail_trigger = close + (close - mid[1]))
}
if flat and dn_b {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = mid[1], target = close - (mid[1] - close) * reward, trail_offset = mid[1] - close, trail_trigger = close - (mid[1] - close))
}
risk.max_drawdown(25)
ph = render.line(hi, "Range high", color = color.translucent(color.amber.300, thin_now ? 20 : 70), width = 1)
pl = render.line(lo, "Range low", color = color.translucent(color.amber.300, thin_now ? 20 : 70), width = 1)
render.band(ph, pl, color = color.translucent(color.amber.500, thin_now ? 88 : 98), opacity = 1, title = "Range")
render.mark(up_b, shape = shape.arrow_up, at = anchor.below_bar, color = color.amber.300, text = "Thin", title = "Thin breakout up")
render.mark(dn_b, shape = shape.arrow_down, at = anchor.above_bar, color = color.amber.300, text = "Thin", title = "Thin breakout down")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Liquidity Impact Breakout", "Strategy · Amihud illiquidity")
pr.row(t, 2, "Illiquidity rank", str.format("{:.0f} / 100", rank), thin_now ? pr.WARN : color.white)
pr.row(t, 3, "Breakouts", thin_now ? "Followed" : "Ignored", color.white)
}Liquidity Wall Bounce
strategy · presets/strategy/liquidity_wall_bounce.qs
Buys the defence of a large resting bid and sells the defence of a large resting ask. A wall is the biggest level within the search window when it is several times the side's average level. The setup needs both views: the bar trades into the wall and closes back on the right side (OHLCV), and the wall is still there at the close with the book leaning the bounce's way (book). The stop sits just beyond the wall: if it is pulled or consumed the idea is wrong. Exit: target at a multiple of the risk, or the opposite wall if it is closer. Needs the order book (L2): backtests run only where the chart has recorded books.
strategy "Liquidity Wall Bounce" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Buys the defence of a large resting bid and sells the defence of a large resting ask. A wall is
/// the biggest level within the search window when it is several times the side's average level.
/// The setup needs both views: the bar trades into the wall and closes back on the right side
/// (OHLCV), and the wall is still there at the close with the book leaning the bounce's way
/// (book). The stop sits just beyond the wall: if it is pulled or consumed the idea is wrong.
/// Exit: target at a multiple of the risk, or the opposite wall if it is closer.
/// Needs the order book (L2): backtests run only where the chart has recorded books.
win_pct = param.float(1.0, title = "Search window (% from mid)", min = 0.05, max = 10, step = 0.05, group = "Walls")
mult = param.float(3.0, title = "Wall size (x average level)", min = 1.2, max = 20, step = 0.1, group = "Walls")
rr = param.float(2.0, title = "Reward / risk", min = 0.5, max = 10, step = 0.1, group = "Exit")
buf = param.float(0.2, title = "Stop buffer (ATR)", min = 0, max = 3, step = 0.05, group = "Exit")
mid = book.mid
(bwp, bws) = book.wall(book.bid_side)
(awp, aws) = book.wall(book.ask_side)
bavg = book.bid_volume() / math.max(book.levels, 1)
aavg = book.ask_volume() / math.max(book.levels, 1)
bid_wall = bws >= mult * bavg and bwp >= mid * (1 - win_pct / 100) ? bwp : na
ask_wall = aws >= mult * aavg and awp <= mid * (1 + win_pct / 100) ? awp : na
a = ta.atr(14)
pres = book.pressure(0.25)
ready = not math.is_na(a)
long_sig = ready and not math.is_na(bid_wall[1]) and not math.is_na(bid_wall) and low <= bid_wall[1] and close > bid_wall[1] and pres > 0
short_sig = ready and not math.is_na(ask_wall[1]) and not math.is_na(ask_wall) and high >= ask_wall[1] and close < ask_wall[1] and pres < 0
flat = position.size == 0
if flat and long_sig {
stop = bid_wall[1] - buf * a
tgt = close + rr * (close - stop)
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = stop, target = math.is_na(ask_wall) ? tgt : math.min(tgt, ask_wall))
}
if flat and short_sig {
stop = ask_wall[1] + buf * a
tgt = close - rr * (stop - close)
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = stop, target = math.is_na(bid_wall) ? tgt : math.max(tgt, bid_wall))
}
risk.max_drawdown(25)
render.step(bid_wall, "Bid wall", color = color.emerald.300, width = 2)
render.step(ask_wall, "Ask wall", color = color.rose.300, width = 2)
render.mark(long_sig and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, size = 0.8, title = "Bid wall bounce")
render.mark(short_sig and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.8, title = "Ask wall rejection")MACD Spectrum Alignment
strategy · presets/strategy/macd_spectrum_alignment.qs
Enters when at least the required number of twelve MACD speeds agree on direction and exits when agreement falls below half of it, with an ATR stop.
strategy "MACD Spectrum Alignment" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Enters when at least the required number of twelve MACD speeds agree on direction and exits
/// when agreement falls below half of it, with an ATR stop.
need = param.int(10, title = "Speeds required (of 12)", min = 1, max = 12)
stopk = param.float(2.5, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1)
score = 0
for f in [3, 5, 6, 8, 10, 12, 15, 18, 22, 27, 33, 40] {
(m, s, h) = ta.macd(close, f, f * 2 + 2, 9)
score := score + (h > 0 ? 1 : -1)
}
a = ta.atr(14)
flat = position.size == 0
if flat and score >= need and not math.is_na(a) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stopk * a)
}
if flat and score <= -need and not math.is_na(a) {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stopk * a)
}
if position.size > 0 and score < need / 2 { exit.close_all() }
if position.size < 0 and score > -need / 2 { exit.close_all() }
risk.max_drawdown(25)
render.candle_color(score >= need ? color.emerald.400 : score <= -need ? color.rose.400 : na)Microprice Momentum
strategy · presets/strategy/microprice_momentum.qs
A short-horizon strategy for intraday charts. It goes with the side the book is about to give way on: the microprice leaning beyond a fraction of the spread, cumulative order-flow imbalance of the top levels positive (negative) over the last bars, and the bar itself closing in its upper (lower) half on rising volume. Trades only when the spread is cheap against the bar range, so the edge is not eaten by crossing the spread. Exit: after a few bars, on an opposite signal, or on an ATR stop. Needs the order book (L1 for the microprice, L2 for order-flow imbalance).
strategy "Microprice Momentum" {
overlay = true
capital = 100000
commission = 0.02
slippage = 1
sizing = percent_equity
size = 20
max_leverage = 1
}
/// A short-horizon strategy for intraday charts. It goes with the side the book is about to give
/// way on: the microprice leaning beyond a fraction of the spread, cumulative order-flow imbalance
/// of the top levels positive (negative) over the last bars, and the bar itself closing in its
/// upper (lower) half on rising volume. Trades only when the spread is cheap against the bar range,
/// so the edge is not eaten by crossing the spread.
/// Exit: after a few bars, on an opposite signal, or on an ATR stop.
/// Needs the order book (L1 for the microprice, L2 for order-flow imbalance).
edge_min = param.float(0.15, title = "Microprice edge (spreads)", min = 0.02, max = 0.5, step = 0.01, group = "Book")
ofi_len = param.int(5, title = "OFI bars", min = 1, max = 100, group = "Book")
max_sp = param.float(0.1, title = "Max spread (x ATR)", min = 0.005, max = 1, step = 0.005, group = "Book")
hold = param.int(5, title = "Bars held", min = 1, max = 100, group = "Exit")
stop_k = param.float(1.5, title = "Stop (ATR)", min = 0.2, max = 10, step = 0.1, group = "Exit")
a = ta.atr(14)
edge = (book.microprice - book.mid) / math.max(book.spread, 1e-12)
ofi_sum = stat.sum(math.coalesce(orderbook.ofi(5), 0), ofi_len)
clv = ((close - low) - (high - close)) / math.max(high - low, 1e-12)
cheap = book.spread < max_sp * a
vol_up = volume > ta.sma(volume, 20)
long_sig = cheap and edge > edge_min and ofi_sum > 0 and clv > 0 and vol_up
short_sig = cheap and edge < -edge_min and ofi_sum < 0 and clv < 0 and vol_up
flat = position.size == 0
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stop_k * a)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stop_k * a)
}
if (position.size > 0 and (short_sig or position.bars_held >= hold)) or (position.size < 0 and (long_sig or position.bars_held >= hold)) { exit.close_all() }
risk.max_drawdown(20)
render.line(book.microprice, "Microprice", color = color.translucent(color.cyan.300, 30), width = 1)
render.mark(long_sig and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.cyan.300, size = 0.6, title = "Book momentum long")
render.mark(short_sig and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.pink.300, size = 0.6, title = "Book momentum short")Momentum Term Pullback
strategy · presets/strategy/momentum_term_pullback.qs · uses viz/premium
Buys dips that the long horizons have not noticed. Momentum is measured on a short and a long horizon band as volatility-scaled t-statistics (return over the horizon divided by the volatility expected over it). When the long band is strongly positive while the short band has gone negative, price is pulling back inside an established trend; the entry waits until the short band turns back up, so the pullback is already ending. Shorts mirror this. Entry: long-band t above long_t, short-band t below -dip_t within the last memory bars and now rising for two bars (mirror for shorts). Exit: ATR stop under the pullback low, target reward R, or the long band losing its sign. Risk: 1 % per trade.
strategy "Momentum Term Pullback" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Buys dips that the long horizons have not noticed. Momentum is measured on a short and a long
/// horizon band as volatility-scaled t-statistics (return over the horizon divided by the
/// volatility expected over it). When the long band is strongly positive while the short band has
/// gone negative, price is pulling back inside an established trend; the entry waits until the
/// short band turns back up, so the pullback is already ending. Shorts mirror this.
/// Entry: long-band t above `long_t`, short-band t below `-dip_t` within the last `memory` bars and
/// now rising for two bars (mirror for shorts). Exit: ATR stop under the pullback low, target
/// `reward` R, or the long band losing its sign. Risk: 1 % per trade.
use "viz/premium" as pr
s_lo = param.int(3, title = "Short band from", min = 1, max = 50, group = "Horizons")
s_hi = param.int(12, title = "Short band to", min = 2, max = 100, group = "Horizons")
l_lo = param.int(60, title = "Long band from", min = 10, max = 500, group = "Horizons")
l_hi = param.int(160, title = "Long band to", min = 20, max = 1000, group = "Horizons")
vol_n = param.int(200, title = "Volatility window", min = 20, max = 5000, group = "Horizons")
long_t = param.float(1.0, title = "Trend strength (t)", min = 0.2, max = 4, step = 0.1, group = "Signals")
dip_t = param.float(0.6, title = "Pullback depth (t)", min = 0.1, max = 4, step = 0.1, group = "Signals")
memory = param.int(8, title = "Pullback memory (bars)", min = 1, max = 100, group = "Signals")
reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
r1 = math.log(close / close[1])
sig = stat.stdev(r1, vol_n)
/// Average t-statistic of the horizons lo .. hi (four evenly spaced samples).
fn band_t(lo: int, hi: int, sig: float) -> float {
acc = 0.0
for j in 0..4 {
h = math.max(1, math.round(lo + (hi - lo) * j / 3))
acc := acc + math.log(close / close[h]) / math.max(sig * math.sqrt(h), 1e-12)
}
return acc / 4
}
st = band_t(s_lo, s_hi, sig)
lt = band_t(l_lo, l_hi, sig)
a = ta.atr(14)
dipped = st < -dip_t
popped = st > dip_t
since_dip = ta.bars_since(dipped)
since_pop = ta.bars_since(popped)
turn_up = st > st[1] and st[1] > st[2]
turn_dn = st < st[1] and st[1] < st[2]
go_l = lt > long_t and math.coalesce(since_dip, 1000000) <= memory and turn_up and st < 0.5
go_s = lt < -long_t and math.coalesce(since_pop, 1000000) <= memory and turn_dn and st > -0.5
lo_n = ta.lowest(low, memory + 2)
hi_n = ta.highest(high, memory + 2)
persist used = 0
if dipped { used := used == 1 ? 0 : used }
if popped { used := used == -1 ? 0 : used }
flat = position.size == 0
if flat and go_l and used != 1 {
stop = math.min(lo_n - 0.25 * a, close - a)
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = stop, target = close + (close - stop) * reward)
used := 1
}
if flat and go_s and used != -1 {
stop = math.max(hi_n + 0.25 * a, close + a)
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = stop, target = close - (stop - close) * reward)
used := -1
}
if position.size > 0 and lt < 0 { exit.close("L") }
if position.size < 0 and lt > 0 { exit.close("S") }
risk.max_drawdown(25)
render.candle_color(lt > long_t ? (st < 0 ? color.teal.300 : na) : lt < -long_t ? (st > 0 ? color.orange.300 : na) : na)
render.mark(go_l and flat and used[1] != 1, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Dip", title = "Long pullback entry")
render.mark(go_s and flat and used[1] != -1, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "Pop", title = "Short pullback entry")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Momentum Term Pullback", "Strategy · short vs long momentum")
pr.row(t, 2, "Long band (t)", str.format("{:+.2f}", lt), lt > 0 ? pr.BULL : pr.BEAR)
pr.row(t, 3, "Short band (t)", str.format("{:+.2f}", st), st > 0 ? pr.BULL : pr.BEAR)
pr.row(t, 4, "Setup", lt > long_t and st < 0 ? "Pullback in uptrend" : lt < -long_t and st > 0 ? "Bounce in downtrend" : "None", pr.WARN)
}Multi-Horizon Momentum
strategy · presets/strategy/multi_horizon_momentum.qs · uses viz/premium
Time-series momentum voted across twelve horizons. Each horizon votes with the sign of its volatility-scaled return; the position follows the vote only when a clear majority agrees, and goes flat when the horizons disagree. Using many horizons avoids betting on one lookback. Entry: net votes at or beyond the majority in either direction. Exit: the vote falls back below the hold level; a protective ATR stop on every entry; one entry per vote episode. Risk: 95 % of equity while invested, 30 % drawdown halt. Method: time-series momentum (Moskowitz, Ooi and Pedersen, 2012).
strategy "Multi-Horizon Momentum" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = percent_equity
size = 95
pyramiding = 1
}
/// Time-series momentum voted across twelve horizons. Each horizon votes with the sign of its
/// volatility-scaled return; the position follows the vote only when a clear majority agrees, and
/// goes flat when the horizons disagree. Using many horizons avoids betting on one lookback.
/// Entry: net votes at or beyond the majority in either direction. Exit: the vote falls back below
/// the hold level; a protective ATR stop on every entry; one entry per vote episode.
/// Risk: 95 % of equity while invested, 30 % drawdown halt.
/// Method: time-series momentum (Moskowitz, Ooi and Pedersen, 2012).
use "viz/premium" as pr
h_min = param.int(12, title = "Shortest horizon", min = 2, max = 200, group = "Vote")
h_step = param.int(12, title = "Horizon step", min = 1, max = 200, group = "Vote")
enter = param.int(8, title = "Majority (of 12)", min = 6, max = 12, group = "Vote")
keep = param.int(4, title = "Hold level", min = 0, max = 11, group = "Vote")
stop_k = param.float(5, title = "Protective stop (ATR)", min = 1, max = 20, step = 0.5, group = "Risk")
sd = math.max(stat.stdev(math.log(close / close[1]), 100), 1e-12)
net = 0
for k in 0..=11 {
hz = h_min + k * h_step
z = math.log(close / close[hz]) / (sd * math.sqrt(hz))
if z > 0.25 { net := net + 1 }
if z < -0.25 { net := net - 1 }
}
a = ta.atr(50)
vote = net >= enter ? 1 : net <= -enter ? -1 : 0
persist used = 0
if vote != vote[1] { used := 0 }
if vote == 1 and position.side <= 0 and used != 1 {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a)
used := 1
}
if vote == -1 and position.side >= 0 and used != -1 {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a)
used := -1
}
if (position.side > 0 and net < keep) or (position.side < 0 and net > -keep) { exit.close_all() }
risk.max_drawdown(30)
render.line(ta.ema(close, h_min + 6 * h_step), "Momentum mean", color = net > 0 ? color.emerald.300 : net < 0 ? color.rose.300 : color.slate.500, width = 2)
render.background(vote == 1 ? color.translucent(color.emerald.500, 93) : vote == -1 ? color.translucent(color.rose.500, 93) : na)
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Multi-Horizon Momentum", "Strategy · 12-horizon vote")
pr.row(t, 2, "Net votes", str.format("{:+d}", net), net > 0 ? pr.BULL : net < 0 ? pr.BEAR : pr.LABEL)
pr.row(t, 3, "Stance", vote > 0 ? "Long" : vote < 0 ? "Short" : "Flat", color.white)
}Order Block Retest
strategy · presets/strategy/order_block_retest.qs · uses smc/structure, smc/zones
After a structure break, waits for price to return into the order block that caused it and enters with a limit order at the block's midpoint; the stop sits beyond the block and the target at the swing extreme. Built on the smc libraries.
strategy "Order Block Retest" {
overlay = true
capital = 100000
commission = 0.05
sizing = risk_percent
size = 0.5
max_objects = 200
}
/// After a structure break, waits for price to return into the order block that caused it and
/// enters with a limit order at the block's midpoint; the stop sits beyond the block and the
/// target at the swing extreme. Built on the smc libraries.
use "smc/structure" as st
use "smc/zones" as zn
legs = param.int(5, title = "Swing legs", min = 2, max = 20)
rr = param.float(2.5, title = "Reward (R)", min = 1, max = 10, step = 0.1)
(dir, up, down, hh, ll) = st.structure(legs)
(found, ob) = zn.order_block(legs)
a = ta.atr(14)
persist zone = zn.Zone()
persist armed = false
if found and position.size == 0 {
zone := ob
armed := true
draw.zone.create(ob.born, ob.top, bar.index + 30, ob.bottom, color = color.translucent(ob.bullish ? color.sky.500 : color.orange.500, 80), text = "OB")
}
if armed and position.size == 0 {
entry = zone.mid()
risk_px = zone.width() * 0.75 + a * 0.25
if zone.bullish {
order.limit("L", side.long, entry)
exit.bracket("L exit", entry = "L", stop = zone.bottom - a * 0.25, target = entry + risk_px * rr)
} else {
order.limit("S", side.short, entry)
exit.bracket("S exit", entry = "S", stop = zone.top + a * 0.25, target = entry - risk_px * rr)
}
}
// a mitigated block is no longer tradeable
if armed and zone.mitigated() {
armed := false
order.cancel_all()
}
if position.size != 0 { armed := false }
render.step(armed ? zone.mid() : na, "Pending entry", color = color.amber.400, stroke = style.dashed)
render.mark(up, shape = shape.tag_up, at = anchor.below_bar, color = color.sky.400, text = "BOS")
render.mark(down, shape = shape.tag_down, at = anchor.above_bar, color = color.orange.400, text = "BOS")OU Half-Life Reversion
strategy · presets/strategy/ou_half_life_reversion.qs · uses viz/premium
Trades mean reversion only when the market is measurably mean-reverting. An Ornstein-Uhlenbeck fit gives the fair value, the z-score of price against it and the half-life of deviations; a stretched z-score is faded only while the half-life is short enough to pay within a few bars, and the trade is given twice the half-life to work. Entry: z beyond the entry level and turning back, half-life below the limit. Exit: target at the fitted mean, stop beyond the stretch, or a time stop at twice the half-life. Risk: 1 % per trade. Method: Uhlenbeck and Ornstein (1930), AR(1) half-life.
strategy "OU Half-Life Reversion" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trades mean reversion only when the market is measurably mean-reverting. An Ornstein-Uhlenbeck
/// fit gives the fair value, the z-score of price against it and the half-life of deviations;
/// a stretched z-score is faded only while the half-life is short enough to pay within a few
/// bars, and the trade is given twice the half-life to work.
/// Entry: z beyond the entry level and turning back, half-life below the limit. Exit: target at
/// the fitted mean, stop beyond the stretch, or a time stop at twice the half-life.
/// Risk: 1 % per trade. Method: Uhlenbeck and Ornstein (1930), AR(1) half-life.
use "viz/premium" as pr
len = param.int(120, title = "Fit window", min = 30, max = 2000, group = "Model")
z_in = param.float(2.0, title = "Entry z-score", min = 0.5, max = 4, step = 0.1, group = "Entry")
hl_max = param.float(30, title = "Maximum half-life (bars)", min = 2, max = 500, group = "Entry")
stop_k = param.float(2.5, title = "Stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
(m, hl, z) = hft.ou(close, len)
a = ta.atr(20)
ok = hl < hl_max and hl > 0
flat = position.size == 0
if flat and ok and z[1] < -z_in and z > z[1] {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = m)
}
if flat and ok and z[1] > z_in and z < z[1] {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = m)
}
persist time_limit = 0
if flat { time_limit := math.round(math.max(2 * math.coalesce(hl, 10), 3)) }
if position.size != 0 and position.bars_held >= time_limit { exit.close_all() }
risk.max_drawdown(25)
render.line(m, "OU mean", color = ok ? color.emerald.300 : color.slate.500, width = 2)
render.mark(flat and ok and z[1] < -z_in and z > z[1], shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Revert", title = "Long setup")
render.mark(flat and ok and z[1] > z_in and z < z[1], shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "Revert", title = "Short setup")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "OU Half-Life Reversion", "Strategy · Ornstein-Uhlenbeck")
pr.row(t, 2, "Half-life", math.is_na(hl) ? "none" : str.format("{:.1f} bars", hl), ok ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Z-score", str.format("{:+.2f}", z), math.abs(z) > z_in ? pr.WARN : color.white)
pr.row(t, 4, "Fair value", str.format("{:.2f}", m), color.white)
}Pressure Divergence Reversal
strategy · presets/strategy/pressure_divergence_reversal.qs · uses viz/premium
Fades exhausted swings that volume pressure did not confirm. Signed volume pressure (buy minus sell volume over total volume, from exchange order flow or a bulk-volume estimate) is averaged into a wave. A new swing low below the previous one, with the wave higher than it was at the previous low, means sellers pushed price further with less force. The entry waits for the wave to cross back over its slower tide line, the sign that buyers have taken over. Entry: bullish pressure divergence at a confirmed swing low, then wave crossing above the tide within window bars (mirror for shorts at swing highs). Exit: stop beyond the divergence swing (at least 1 ATR), target reward R, or time stop. Risk: 1 % per trade.
strategy "Pressure Divergence Reversal" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Fades exhausted swings that volume pressure did not confirm. Signed volume pressure (buy minus
/// sell volume over total volume, from exchange order flow or a bulk-volume estimate) is averaged
/// into a wave. A new swing low below the previous one, with the wave higher than it was at the
/// previous low, means sellers pushed price further with less force. The entry waits for the wave
/// to cross back over its slower tide line, the sign that buyers have taken over.
/// Entry: bullish pressure divergence at a confirmed swing low, then wave crossing above the tide
/// within `window` bars (mirror for shorts at swing highs). Exit: stop beyond the divergence swing
/// (at least 1 ATR), target `reward` R, or time stop. Risk: 1 % per trade.
use "viz/premium" as pr
fast = param.int(8, title = "Wave length", min = 2, max = 200, group = "Pressure")
slow = param.int(34, title = "Tide length", min = 5, max = 500, group = "Pressure")
swing = param.int(5, title = "Swing bars", min = 2, max = 30, group = "Divergence")
reach = param.int(60, title = "Divergence reach (bars)", min = 10, max = 500, group = "Divergence")
window = param.int(10, title = "Confirmation window (bars)", min = 1, max = 50, group = "Signals")
reward = param.float(2.0, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
hold = param.int(72, title = "Maximum bars held", min = 2, max = 1000, group = "Risk")
(buy, sell, _) = hft.flow(fast)
signed = buy - sell
wave = ta.ema(signed, fast) / math.max(ta.ema(volume, fast), 1e-12)
tide = ta.ema(signed, slow) / math.max(ta.ema(volume, slow), 1e-12)
ph = ta.pivot_high(swing, swing)
pl = ta.pivot_low(swing, swing)
wave_at = wave[swing]
persist ph_p = na
persist ph_w = na
persist ph_x = 0
persist pl_p = na
persist pl_w = na
persist pl_x = 0
persist bull_x = -1
persist bear_x = -1
persist bull_stop = na
persist bear_stop = na
if not math.is_na(pl) {
if not math.is_na(pl_p) and bar.index - pl_x <= reach and pl < pl_p and wave_at > pl_w and pl_w < 0 {
bull_x := bar.index
bull_stop := pl
}
pl_p := pl
pl_w := wave_at
pl_x := bar.index
}
if not math.is_na(ph) {
if not math.is_na(ph_p) and bar.index - ph_x <= reach and ph > ph_p and wave_at < ph_w and ph_w > 0 {
bear_x := bar.index
bear_stop := ph
}
ph_p := ph
ph_w := wave_at
ph_x := bar.index
}
a = ta.atr(14)
x_up = ta.cross_up(wave, tide)
x_dn = ta.cross_down(wave, tide)
go_l = bull_x >= 0 and bar.index - bull_x <= window and x_up
go_s = bear_x >= 0 and bar.index - bear_x <= window and x_dn
flat = position.size == 0
if flat and go_l {
stop = math.min(bull_stop - 0.2 * a, close - a)
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = stop, target = close + (close - stop) * reward)
bull_x := -1
}
if flat and go_s {
stop = math.max(bear_stop + 0.2 * a, close + a)
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = stop, target = close - (stop - close) * reward)
bear_x := -1
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
render.mark(not math.is_na(pl) and bull_x == bar.index, shape = shape.circle, at = anchor.below_bar, color = color.emerald.200, size = 0.5, title = "Bullish divergence")
render.mark(not math.is_na(ph) and bear_x == bar.index, shape = shape.circle, at = anchor.above_bar, color = color.rose.200, size = 0.5, title = "Bearish divergence")
render.mark(go_l and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Div", title = "Long entry")
render.mark(go_s and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "Div", title = "Short entry")
render.candle_color(color.ramp(math.clamp(wave, -0.5, 0.5), -0.5, 0.5, "pnl"))
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Pressure Divergence Reversal", "Strategy · signed volume pressure")
pr.row(t, 2, "Wave / tide", str.format("{:+.2f} / {:+.2f}", wave, tide), wave > tide ? pr.BULL : pr.BEAR)
pr.row(t, 3, "Bullish setup", bull_x >= 0 and bar.index - bull_x <= window ? "Armed" : "No", pr.BULL)
pr.row(t, 4, "Bearish setup", bear_x >= 0 and bar.index - bear_x <= window ? "Armed" : "No", pr.BEAR)
}Queue Divergence Reversal
strategy · presets/strategy/queue_divergence_reversal.qs
Buys when price has fallen for several bars while the bid queue at the touch kept growing (support building) and the bar closes up; sells the mirror. Exit at a reward multiple or ATR stop. Needs the order book (L1): trades only on bars with a recorded book.
strategy "Queue Divergence Reversal" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Buys when price has fallen for several bars while the bid queue at the touch kept growing
/// (support building) and the bar closes up; sells the mirror. Exit at a reward multiple or ATR
/// stop. Needs the order book (L1): trades only on bars with a recorded book.
lb = param.int(4, title = "Decline bars", min = 2, max = 50)
rr = param.float(1.5, title = "Reward / risk", min = 0.5, max = 10, step = 0.1)
stopk = param.float(1.2, title = "Stop (ATR)", min = 0.2, max = 10, step = 0.1)
bp = book.bid
bq = book.bid_size
ap = book.ask
aq = book.ask_size
bid_flow = math.is_na(bp[1]) ? na : bp > bp[1] ? bq : bp < bp[1] ? -bq[1] : bq - bq[1]
ask_flow = math.is_na(ap[1]) ? na : ap < ap[1] ? aq : ap > ap[1] ? -aq[1] : aq - aq[1]
net = math.coalesce(bid_flow, 0) - math.coalesce(ask_flow, 0)
flow = stat.sum(net, lb)
a = ta.atr(14)
long_sig = not math.is_na(a) and close[1] < close[lb] and flow > 0 and close > open and not math.is_na(bp)
short_sig = not math.is_na(a) and close[1] > close[lb] and flow < 0 and close < open and not math.is_na(bp)
flat = position.size == 0
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stopk * a, target = close + rr * stopk * a)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stopk * a, target = close - rr * stopk * a)
}
risk.max_drawdown(25)
render.mark(long_sig and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.teal.300, size = 0.6, title = "Bids building")
render.mark(short_sig and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.6, title = "Asks building")Regime Probability Allocator
strategy · presets/strategy/regime_probability_allocator.qs · uses viz/premium
Holds a position only when one regime is clearly more probable. Trend (regression slope in ATR units), trend quality (R squared) and order (one minus return entropy) are combined and turned into bull / neutral / bear probabilities with a softmax; the strategy is long while the bull probability exceeds the confidence level, short while the bear probability does, flat otherwise. Entry: a regime passes the confidence level. Exit: the regime loses confidence (falls below the exit level) or the opposite regime takes over; a protective ATR stop on every entry. Risk: 95 % of equity while invested, 30 % drawdown halt.
strategy "Regime Probability Allocator" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = percent_equity
size = 95
pyramiding = 1
}
/// Holds a position only when one regime is clearly more probable. Trend (regression slope in ATR
/// units), trend quality (R squared) and order (one minus return entropy) are combined and turned
/// into bull / neutral / bear probabilities with a softmax; the strategy is long while the bull
/// probability exceeds the confidence level, short while the bear probability does, flat otherwise.
/// Entry: a regime passes the confidence level. Exit: the regime loses confidence (falls below
/// the exit level) or the opposite regime takes over; a protective ATR stop on every entry.
/// Risk: 95 % of equity while invested, 30 % drawdown halt.
use "viz/premium" as pr
len = param.int(50, title = "Regression window", min = 10, max = 500, group = "Factors")
ent = param.int(60, title = "Entropy window", min = 20, max = 1000, group = "Factors")
temp = param.float(1.0, title = "Softmax temperature", min = 0.2, max = 5, step = 0.1, group = "Model")
conf = param.float(60, title = "Entry confidence (%)", min = 40, max = 95, group = "Signals")
exit_c = param.float(45, title = "Exit confidence (%)", min = 20, max = 90, group = "Signals")
stop_k = param.float(4, title = "Protective stop (ATR)", min = 1, max = 15, step = 0.5, group = "Risk")
(_, slope, _) = stat.linreg(close, len, 0)
a = ta.atr(len)
sl = slope / math.max(a, 1e-12) * math.sqrt(len)
q = math.coalesce(stat.r_squared(close, len), 0)
o = 1 - math.coalesce(stat.entropy(math.log(close / close[1]), ent, 10), 1)
strength = q * (1 + 3 * o)
bull_s = sl * strength / temp
bear_s = -sl * strength / temp
neut_s = (1 - q) * 1.5 / temp
mx = math.max(math.max(bull_s, bear_s), neut_s)
eb = math.exp(bull_s - mx)
en = math.exp(neut_s - mx)
er = math.exp(bear_s - mx)
pb = 100 * eb / (eb + en + er)
prb = 100 * er / (eb + en + er)
// one entry per regime episode: after a stop, wait until the regime resets
regime = pb > conf ? 1 : prb > conf ? -1 : 0
persist used = 0
if regime != regime[1] { used := 0 }
if position.size <= 0 and regime == 1 and used != 1 {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a)
used := 1
}
if position.size >= 0 and regime == -1 and used != -1 {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a)
used := -1
}
if (position.side > 0 and pb < exit_c) or (position.side < 0 and prb < exit_c) { exit.close_all() }
risk.max_drawdown(30)
render.background(pb > conf ? color.translucent(color.emerald.500, 92) : prb > conf ? color.translucent(color.rose.500, 92) : na)
render.line(ta.ema(close, len), "Regime mean", color = pb > prb ? color.emerald.300 : color.rose.300, width = 2)
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Regime Probability Allocator", "Strategy · softmax regime model")
pr.row(t, 2, "Bull probability", str.format("{:.0f} %", pb), pr.BULL)
pr.row(t, 3, "Bear probability", str.format("{:.0f} %", prb), pr.BEAR)
pr.row(t, 4, "Allocation", position.side > 0 ? "Long" : position.side < 0 ? "Short" : "Cash", color.white)
}Regime Switching
strategy · presets/strategy/regime_switching.qs · uses stats/filters
Trend-follows (EMA cross) in a trending regime and fades RSI extremes in a ranging regime; stands aside when the regime is noisy. Exits whenever the regime changes.
strategy "Regime Switching" {
overlay = true
capital = 100000
commission = 0.05
sizing = percent_equity
size = 60
}
/// Trend-follows (EMA cross) in a trending regime and fades RSI extremes in a ranging regime;
/// stands aside when the regime is noisy. Exits whenever the regime changes.
use "stats/filters" as flt
r = flt.regime(100)
fast = ta.ema(close, 20)
slow = ta.ema(close, 50)
rsi = ta.rsi(close, 14)
cross_up = ta.cross_up(fast, slow)
cross_dn = ta.cross_down(fast, slow)
rsi_up = ta.cross_up(rsi, 30)
rsi_dn = ta.cross_down(rsi, 70)
if r != r[1] and position.size != 0 { exit.close_all() }
if r == flt.Regime.trend {
if cross_up { order.market("TL", side.long) }
if cross_dn { order.market("TS", side.short) }
}
if r == flt.Regime.range and position.size == 0 {
if rsi_up { order.market("RL", side.long) }
if rsi_dn { order.market("RS", side.short) }
}
if r == flt.Regime.range and position.size > 0 and rsi > 55 { exit.close_all() }
if r == flt.Regime.range and position.size < 0 and rsi < 45 { exit.close_all() }
render.line(fast, "Fast", color = color.sky.400)
render.line(slow, "Slow", color = color.orange.400)
render.background(r == flt.Regime.trend ? color.translucent(color.emerald.500, 92) : r == flt.Regime.range ? color.translucent(color.sky.500, 94) : color.translucent(color.slate.500, 90))Regression Breakout Retest
strategy · presets/strategy/regression_breakout_retest.qs · uses viz/premium
Breaks of a clean regression channel against its own slope start new trends; the safest entry is the retest of the broken edge. A falling channel (R squared above the level) whose upper 2-sigma edge is closed through arms a long setup at that edge; when price pulls back to within the tolerance of the frozen edge and closes above it, the strategy buys. Mirror for shorts. Exit: stop below the retest low by half an ATR, target of the channel height projected from the edge, setups expire after a number of bars.
strategy "Regression Breakout Retest" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Breaks of a clean regression channel against its own slope start new trends; the safest entry is
/// the retest of the broken edge. A falling channel (R squared above the level) whose upper 2-sigma
/// edge is closed through arms a long setup at that edge; when price pulls back to within the
/// tolerance of the frozen edge and closes above it, the strategy buys. Mirror for shorts.
/// Exit: stop below the retest low by half an ATR, target of the channel height projected from
/// the edge, setups expire after a number of bars.
use "viz/premium" as pr
len = param.int(120, title = "Channel window", min = 20, max = 2000, group = "Channel")
q_min = param.float(0.55, title = "Minimum R squared", min = 0.1, max = 0.95, step = 0.01, group = "Channel")
k_sd = param.float(2.0, title = "Edge (sigma)", min = 0.5, max = 4, step = 0.1, group = "Channel")
tol = param.float(0.3, title = "Retest tolerance (ATR)", min = 0.05, max = 2, step = 0.05, group = "Entry")
expiry = param.int(30, title = "Setup expiry (bars)", min = 2, max = 500, group = "Entry")
(v, s, _) = stat.linreg(close, len, 0)
sd = stat.stdev(close - v, len)
q = stat.r_squared(close, len)
a = ta.atr(14)
up_edge = v + k_sd * sd
dn_edge = v - k_sd * sd
persist long_lvl = na
persist long_h = na
persist long_age = 0
persist short_lvl = na
persist short_h = na
persist short_age = 0
if q[1] > q_min and s[1] < 0 and close > up_edge[1] and close[1] <= up_edge[2] {
long_lvl := up_edge[1]
long_h := 2 * k_sd * sd[1]
long_age := 0
}
if q[1] > q_min and s[1] > 0 and close < dn_edge[1] and close[1] >= dn_edge[2] {
short_lvl := dn_edge[1]
short_h := 2 * k_sd * sd[1]
short_age := 0
}
long_age += 1
short_age += 1
if long_age > expiry { long_lvl := na }
if short_age > expiry { short_lvl := na }
flat = position.size == 0
long_sig = not math.is_na(long_lvl) and long_age > 1 and low <= long_lvl + tol * a and close > long_lvl
short_sig = not math.is_na(short_lvl) and short_age > 1 and high >= short_lvl - tol * a and close < short_lvl
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = low - 0.5 * a, target = long_lvl + long_h)
long_lvl := na
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = high + 0.5 * a, target = short_lvl - short_h)
short_lvl := na
}
risk.max_drawdown(25)
pu = render.line(up_edge, "Upper edge", color = color.translucent(color.rose.300, 45), width = 1)
pc = render.line(v, "Regression", color = q > q_min ? (s > 0 ? color.sky.300 : color.amber.300) : color.slate.500, width = 1.5)
pd = render.line(dn_edge, "Lower edge", color = color.translucent(color.emerald.300, 45), width = 1)
render.band(pu, pd, color = color.indigo.500, opacity = 0.05, title = "Channel")
render.step(long_lvl, "Long retest level", color = color.emerald.300, width = 2)
render.step(short_lvl, "Short retest level", color = color.rose.300, width = 2)
render.mark(long_sig and flat, shape = shape.tag_up, at = anchor.below_bar, color = color.emerald.400, text = "Retest", title = "Retest buy")
render.mark(short_sig and flat, shape = shape.tag_down, at = anchor.above_bar, color = color.rose.400, text = "Retest", title = "Retest sell")Regression Pullback
strategy · presets/strategy/regression_pullback.qs · uses viz/premium
Buys pullbacks to the lower edge of a clean rising regression channel and sells rallies to the upper edge of a clean falling one. "Clean" means the channel explains price well (R squared above the quality level), so the edges are statistically meaningful; pullbacks in noisy channels are ignored. Entry: R squared above the level, slope in the trade's direction, low touching the lower 2-sigma band and the bar closing back inside (mirror for shorts). Exit: target at the channel centre plus half the band, stop beyond the 3-sigma band, time stop. Risk: 1 % per trade.
strategy "Regression Pullback" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Buys pullbacks to the lower edge of a clean rising regression channel and sells rallies to the
/// upper edge of a clean falling one. "Clean" means the channel explains price well (R squared
/// above the quality level), so the edges are statistically meaningful; pullbacks in noisy
/// channels are ignored.
/// Entry: R squared above the level, slope in the trade's direction, low touching the lower 2-sigma
/// band and the bar closing back inside (mirror for shorts). Exit: target at the channel centre
/// plus half the band, stop beyond the 3-sigma band, time stop. Risk: 1 % per trade.
use "viz/premium" as pr
len = param.int(100, title = "Channel window", min = 20, max = 1000, group = "Channel")
q_min = param.float(0.55, title = "Minimum R squared", min = 0.1, max = 0.95, step = 0.01, group = "Channel")
k_in = param.float(2.0, title = "Entry band (sigma)", min = 0.5, max = 4, step = 0.1, group = "Entry")
hold = param.int(40, title = "Maximum bars held", min = 2, max = 500, group = "Exit")
(v, slope, _) = stat.linreg(close, len, 0)
sd = stat.stdev(close - v, len)
q = stat.r_squared(close, len)
clean = q > q_min
up = slope > 0
lo = v - k_in * sd
hi = v + k_in * sd
flat = position.size == 0
long_sig = clean and up and low <= lo and close > lo
short_sig = clean and not up and high >= hi and close < hi
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = v - 3 * sd, target = v + 0.5 * sd)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = v + 3 * sd, target = v - 0.5 * sd)
}
if position.size != 0 and position.bars_held >= hold { exit.close_all() }
risk.max_drawdown(25)
pu = render.line(hi, "Upper band", color = color.translucent(color.amber.300, 40), width = 1)
pc = render.line(v, "Regression", color = clean ? (up ? color.sky.300 : color.amber.300) : color.slate.500, width = 2)
pl = render.line(lo, "Lower band", color = color.translucent(color.sky.300, 40), width = 1)
render.band(pu, pc, texture = "gradient", direction = "edges", upper_color = color.amber.500, lower_color = color.slate.900, opacity = 0.18, title = "Upper half")
render.band(pc, pl, texture = "gradient", direction = "edges", upper_color = color.slate.900, lower_color = color.sky.500, opacity = 0.18, title = "Lower half")
render.mark(long_sig, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, size = 0.7, title = "Pullback buy")
render.mark(short_sig, shape = shape.triangle_down, at = anchor.above_bar, color = color.amber.300, size = 0.7, title = "Rally sell")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Regression Pullback", "Strategy · channel-edge entries")
pr.row(t, 2, "R squared", str.format("{:.2f}", q), clean ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Channel", clean ? (up ? "Clean rising" : "Clean falling") : "Noisy", color.white)
}Regression Supertrend Trader
strategy · presets/strategy/regression_supertrend_trader.qs
Always-in trend trading on a supertrend that trails the regression line: long on a bull flip, short on a bear flip, with the trailing stop as the protective exit.
strategy "Regression Supertrend Trader" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Always-in trend trading on a supertrend that trails the regression line: long on a bull flip,
/// short on a bear flip, with the trailing stop as the protective exit.
len = param.int(30, title = "Regression window", min = 5, max = 500)
factor = param.float(2.5, title = "ATR factor", min = 0.5, max = 10, step = 0.1)
(reg, _, _) = stat.linreg(close, len, 0)
a = ta.atr(14)
persist up = na
persist dn = na
persist dir = 1
up := math.is_na(up) ? reg - factor * a : reg[1] > up ? math.max(reg - factor * a, up) : reg - factor * a
dn := math.is_na(dn) ? reg + factor * a : reg[1] < dn ? math.min(reg + factor * a, dn) : reg + factor * a
if dir == 1 and reg < up { dir := -1 }
else if dir == -1 and reg > dn { dir := 1 }
stop = dir == 1 ? up : dn
ready = not math.is_na(stop)
if ready and dir == 1 and dir[1] == -1 {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = stop)
}
if ready and dir == -1 and dir[1] == 1 {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = stop)
}
risk.max_drawdown(30)
render.line(dir == 1 ? stop : na, "Bull stop", color = color.emerald.400, width = 2)
render.line(dir == -1 ? stop : na, "Bear stop", color = color.rose.400, width = 2)Skew Regime Trend
strategy · presets/strategy/skew_regime_trend.qs
Trend following that respects the shape of returns: longs on EMA crosses only while returns are right-skewed (gains come in bursts), shorts only while they are left-skewed (losses come in bursts). Exit on the opposite cross or an ATR stop.
strategy "Skew Regime Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Trend following that respects the shape of returns: longs on EMA crosses only while returns are
/// right-skewed (gains come in bursts), shorts only while they are left-skewed (losses come in
/// bursts). Exit on the opposite cross or an ATR stop.
fast = param.int(20, title = "Fast EMA", min = 2, max = 500)
slow = param.int(60, title = "Slow EMA", min = 5, max = 1000)
win = param.int(100, title = "Skew window", min = 20, max = 2000)
stopk = param.float(2.5, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1)
r = math.log(close / close[1])
sk = stat.skew(r, win)
f = ta.ema(close, fast)
s = ta.ema(close, slow)
a = ta.atr(14)
flat = position.size == 0
if flat and ta.cross_up(f, s) and sk > 0 and not math.is_na(a) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stopk * a)
}
if flat and ta.cross_down(f, s) and sk < 0 and not math.is_na(a) {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stopk * a)
}
if position.size > 0 and ta.cross_down(f, s) { exit.close_all() }
if position.size < 0 and ta.cross_up(f, s) { exit.close_all() }
risk.max_drawdown(25)
render.line(f, "Fast", color = sk > 0 ? color.lime.300 : color.red.300, width = 1.5)
render.line(s, "Slow", color = color.translucent(color.slate.300, 30), width = 1.5)Slope Spectrum Trend
strategy · presets/strategy/slope_spectrum_trend.qs · uses viz/premium
Trend following that waits until the trend is statistically real on most horizons. Sixteen regression slopes (from short to long) are tested with their t-statistics; the strategy goes long when the share of significantly rising horizons crosses the entry level and short on the mirror condition. A chandelier stop trails the position, and the trade ends when the share falls back below the exit level (the trend is no longer significant).
strategy "Slope Spectrum Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Trend following that waits until the trend is statistically real on most horizons. Sixteen
/// regression slopes (from short to long) are tested with their t-statistics; the strategy goes
/// long when the share of significantly rising horizons crosses the entry level and short on the
/// mirror condition. A chandelier stop trails the position, and the trade ends when the share
/// falls back below the exit level (the trend is no longer significant).
use "viz/premium" as pr
first = param.int(10, title = "Shortest horizon", min = 4, max = 200, group = "Spectrum")
gap = param.int(12, title = "Horizon step", min = 1, max = 100, group = "Spectrum")
t_sig = param.float(2.0, title = "Significant |t|", min = 1, max = 6, step = 0.1, group = "Spectrum")
enter = param.float(0.6, title = "Entry share", min = 0.1, max = 1, step = 0.05, group = "Signals")
leave = param.float(0.2, title = "Exit share", min = 0, max = 0.9, step = 0.05, group = "Signals")
ch_k = param.float(3.0, title = "Chandelier (ATR)", min = 1, max = 10, step = 0.1, group = "Exit")
share = 0.0
for k in 0..=15 {
len = first + k * gap
(v, s, _) = stat.linreg(close, len, 0)
sd = stat.stdev(close - v, len)
sxx = len * (len * len - 1) / 12
tt = s / math.max(sd / math.sqrt(sxx), 1e-12)
share := share + (tt > t_sig ? 1 : tt < -t_sig ? -1 : 0)
}
share := share / 16
(long_stop, short_stop) = ta.chandelier(22, ch_k)
go_long = share > enter and share[1] <= enter
go_short = share < -enter and share[1] >= -enter
flat = position.size == 0
if flat and go_long { order.market("L", side.long) }
if flat and go_short { order.market("S", side.short) }
if position.size > 0 and (share < leave or close < long_stop[1]) { exit.close_all() }
if position.size < 0 and (share > -leave or close > short_stop[1]) { exit.close_all() }
risk.max_drawdown(25)
render.line(position.size > 0 ? long_stop : na, "Long stop", color = color.emerald.300, width = 1.5)
render.line(position.size < 0 ? short_stop : na, "Short stop", color = color.rose.300, width = 1.5)
render.candle_color(share > enter ? color.emerald.400 : share < -enter ? color.rose.400 : na)
render.mark(go_long and flat, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, size = 0.8, title = "Significant up-trend")
render.mark(go_short and flat, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.8, title = "Significant down-trend")Spectral Cycle Timing
strategy · presets/strategy/spectral_cycle_timing.qs · uses viz/premium
Times entries with the market's dominant cycle. Complex demodulation measures power at 16 cycle lengths; when one length clearly dominates (a cycle lock), the phase of that cycle tells where price sits in it. The strategy buys cycle troughs and sells cycle crests in the direction of the slower trend, and leaves when half a cycle has passed. Entry: cycle lock, the detrended cycle wave turning up from below zero (trough) with price above the trend EMA, or down from above zero (crest) with price below it. Exit: half the dominant period, an ATR stop, or a target at the cycle amplitude. Risk: 1 % per trade. Method: complex demodulation (Bloomfield, 1976).
strategy "Spectral Cycle Timing" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Times entries with the market's dominant cycle. Complex demodulation measures power at 16 cycle
/// lengths; when one length clearly dominates (a cycle lock), the phase of that cycle tells where
/// price sits in it. The strategy buys cycle troughs and sells cycle crests in the direction of
/// the slower trend, and leaves when half a cycle has passed.
/// Entry: cycle lock, the detrended cycle wave turning up from below zero (trough) with price above
/// the trend EMA, or down from above zero (crest) with price below it. Exit: half the dominant
/// period, an ATR stop, or a target at the cycle amplitude. Risk: 1 % per trade.
/// Method: complex demodulation (Bloomfield, 1976).
use "viz/premium" as pr
smooth = param.int(48, title = "Demodulation smoothing", min = 8, max = 400, group = "Spectrum")
p_min = param.int(10, title = "Shortest period", min = 4, max = 80, group = "Spectrum")
p_step = param.int(3, title = "Period step", min = 1, max = 20, group = "Spectrum")
lock = param.float(0.14, title = "Lock share", min = 0.07, max = 0.5, step = 0.01, group = "Spectrum")
trend = param.int(150, title = "Trend EMA", min = 20, max = 1000, group = "Trend")
stop_k = param.float(2.0, title = "Stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
x = close - ta.sma(close, smooth)
best = 0.0
best_k = 0
total = 0.0
for k in 0..=15 {
per = p_min + k * p_step
ang = 2 * math.pi * bar.index / per
re = ta.ema(x * math.cos(ang), smooth)
im = ta.ema(x * math.sin(ang), smooth)
p = math.coalesce(re * re + im * im, 0)
total := total + p
if p > best {
best := p
best_k := k
}
}
share = total > 0 ? best / total : 0
period = p_min + best_k * p_step
// the cycle wave: price detrended over 20 bars and smoothed over 5, read against the locked cycle
wave = ta.ema(close - ta.sma(close, 20), 5)
t = ta.ema(close, trend)
a = ta.atr(20)
locked = share >= lock
trough = locked and wave[1] < wave[2] and wave > wave[1] and wave < 0
crest = locked and wave[1] > wave[2] and wave < wave[1] and wave > 0
flat = position.size == 0
if flat and trough and close > t {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = close + 2 * math.abs(wave) + a)
}
if flat and crest and close < t {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = close - 2 * math.abs(wave) - a)
}
persist half = 10
if flat { half := math.max(math.round(period / 2), 3) }
if position.size != 0 and position.bars_held >= half { exit.close_all() }
risk.max_drawdown(25)
render.line(t, "Trend EMA", color = color.amber.300, width = 1.5)
render.mark(trough and close > t, shape = shape.circle, at = anchor.below_bar, color = color.cyan.300, size = 0.6, title = "Cycle trough")
render.mark(crest and close < t, shape = shape.circle, at = anchor.above_bar, color = color.fuchsia.300, size = 0.6, title = "Cycle crest")
persist tb = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(tb, 2, "Spectral Cycle Timing", "Strategy · dominant-cycle entries")
pr.row(tb, 2, "Dominant period", str.format("{} bars", period), color.white)
pr.row(tb, 3, "Power share", str.format("{:.0%}", share), locked ? pr.BULL : pr.LABEL)
pr.row(tb, 4, "Phase", wave > 0 ? "Upper half" : "Lower half", color.white)
}Squeeze Breakout
strategy · presets/strategy/squeeze_breakout.qs
Enters when a Bollinger-inside-Keltner squeeze releases in the direction of momentum; exits on a Chandelier stop or after max_bars bars.
strategy "Squeeze Breakout" {
overlay = true
capital = 25000
commission = 0.02
sizing = percent_equity
size = 95
}
/// Enters when a Bollinger-inside-Keltner squeeze releases in the direction of momentum;
/// exits on a Chandelier stop or after `max_bars` bars.
len = param.int(20, title = "Length", min = 5)
max_bars = param.int(40, title = "Max bars in trade", min = 1)
(_, bb_up, bb_lo) = ta.bollinger(close, len, 2.0)
(_, kc_up, kc_lo) = ta.keltner(close, len, 1.5, len)
on = bb_up < kc_up and bb_lo > kc_lo
released = on[1] and not on
(mom, _, _) = stat.linreg(close - ta.sma(close, len), len, 0)
(long_stop, short_stop) = ta.chandelier(22, 3.0)
if position.size == 0 and released {
if mom > 0 { order.market("L", side.long) }
if mom < 0 { order.market("S", side.short) }
}
if position.size > 0 and (close < long_stop or position.bars_held >= max_bars) { exit.close("L") }
if position.size < 0 and (close > short_stop or position.bars_held >= max_bars) { exit.close("S") }
render.step(position.size > 0 ? long_stop : na, "Long stop", color = color.emerald.500)
render.step(position.size < 0 ? short_stop : na, "Short stop", color = color.rose.500)
render.background(on ? color.translucent(color.amber.500, 90) : na)
render.mark(released and mom > 0, shape = shape.arrow_up, at = anchor.below_bar, color = color.lime.400, text = "Release")
render.mark(released and mom < 0, shape = shape.arrow_down, at = anchor.above_bar, color = color.red.400, text = "Release")Trend Breakout (ATR)
strategy · presets/strategy/trend_breakout_atr.qs · uses risk/position_sizing
Donchian breakout in the direction of a 200-EMA trend filter. Size risks 1% of equity on an ATR stop; the target is reward times the risk and a trailing stop takes over after 1R.
strategy "Trend Breakout (ATR)" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
pyramiding = 1
}
/// Donchian breakout in the direction of a 200-EMA trend filter. Size risks 1% of equity on an
/// ATR stop; the target is `reward` times the risk and a trailing stop takes over after 1R.
use "risk/position_sizing" as ps
len = param.int(20, title = "Breakout length", min = 5, max = 200)
trend = param.int(200, title = "Trend EMA", min = 20, max = 1000)
stop_k = param.float(2.0, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1)
reward = param.float(3.0, title = "Reward (R)", min = 0.5, max = 10, step = 0.1)
(_, upper, lower) = ta.donchian(len)
filter = ta.ema(close, trend)
a = ta.atr(14)
long_sig = close > upper[1] and close > filter
short_sig = close < lower[1] and close < filter
if position.size == 0 and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = close + stop_k * a * reward,
trail_offset = stop_k * a, trail_trigger = close + stop_k * a)
}
if position.size == 0 and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = close - stop_k * a * reward,
trail_offset = stop_k * a, trail_trigger = close - stop_k * a)
}
risk.max_drawdown(25)
risk.max_trades_per_day(4)
render.line(upper, "Upper", color = color.teal.500, width = 1)
render.line(lower, "Lower", color = color.rose.500, width = 1)
render.line(filter, "Trend filter", color = color.amber.400, width = 2)
render.mark(long_sig and position.size == 0, shape = shape.arrow_up, at = anchor.below_bar, color = color.teal.400, text = "Long")
render.mark(short_sig and position.size == 0, shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.400, text = "Short")Trend Prism Follower
strategy · presets/strategy/trend_prism_follower.qs · uses viz/premium
Follows trends only while they are both unanimous and clean. Eight linear regressions from base to eight times base bars vote on direction with their slopes, and their mean R squared measures how straight the trend is. A position opens when every slope agrees and the fits are clean, and closes as soon as quality decays or the shortest fits turn against it, so the system sits out the noisy middle of ranges where trend followers lose most. Entry: all eight slopes up (down) and mean R squared above q_in. Exit: mean R squared below q_out, the two shortest slopes turning against the position, or a stop stop_k ATR beyond the longest regression (trailed, never loosened). Risk: 1 % per trade at the initial stop.
strategy "Trend Prism Follower" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Follows trends only while they are both unanimous and clean. Eight linear regressions from
/// `base` to eight times `base` bars vote on direction with their slopes, and their mean R squared
/// measures how straight the trend is. A position opens when every slope agrees and the fits are
/// clean, and closes as soon as quality decays or the shortest fits turn against it, so the system
/// sits out the noisy middle of ranges where trend followers lose most.
/// Entry: all eight slopes up (down) and mean R squared above `q_in`. Exit: mean R squared below
/// `q_out`, the two shortest slopes turning against the position, or a stop `stop_k` ATR beyond
/// the longest regression (trailed, never loosened). Risk: 1 % per trade at the initial stop.
use "viz/premium" as pr
base = param.int(12, title = "Shortest window", min = 4, max = 200, group = "Prism")
q_in = param.float(0.6, title = "Entry quality (R squared)", min = 0.1, max = 0.95, step = 0.05, group = "Signals")
q_out = param.float(0.35, title = "Exit quality (R squared)", min = 0.05, max = 0.9, step = 0.05, group = "Signals")
stop_k = param.float(1.5, title = "Stop beyond long fit (ATR)", min = 0.2, max = 6, step = 0.1, group = "Risk")
pos = 0
neg = 0
q_sum = 0.0
s1 = 0.0
s2 = 0.0
v8 = na
prev = render.line(close, "Facet edge 0", color = color.transparent, width = 0)
for k in 1..=8 {
(v, sl, _) = stat.linreg(close, base * k, 0)
q = math.coalesce(stat.r_squared(close, base * k), 0)
if sl > 0 { pos := pos + 1 }
if sl < 0 { neg := neg + 1 }
if k == 1 { s1 := sl }
if k == 2 { s2 := sl }
if k == 8 { v8 := v }
q_sum := q_sum + q
edge = render.line(v, str.format("Regression {}", k), color = color.transparent, width = 0)
render.band(prev, edge, color = color.ramp(q, 0, 1, "viridis", 45 + k * 4), opacity = 1, title = str.format("Facet {}", k))
prev := edge
}
quality = q_sum / 8
a = ta.atr(14)
go_l = pos == 8 and quality >= q_in
go_s = neg == 8 and quality >= q_in
persist used = 0
if quality < q_out { used := 0 }
persist trail = na
flat = position.size == 0
if flat and go_l and used != 1 {
trail := math.min(v8 - stop_k * a, close - a)
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = trail)
used := 1
}
if flat and go_s and used != -1 {
trail := math.max(v8 + stop_k * a, close + a)
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = trail)
used := -1
}
if position.size > 0 {
trail := math.max(trail, v8 - stop_k * a)
exit.bracket("L stop", entry = "L", stop = trail)
if quality < q_out or (s1 < 0 and s2 < 0) { exit.close("L") }
}
if position.size < 0 {
trail := math.min(trail, v8 + stop_k * a)
exit.bracket("S stop", entry = "S", stop = trail)
if quality < q_out or (s1 > 0 and s2 > 0) { exit.close("S") }
}
risk.max_drawdown(25)
render.step(position.size != 0 ? trail : na, "Trailing stop", color = color.translucent(color.white, 30), width = 1, stroke = style.dashed)
render.mark(go_l and flat and used[1] != 1, shape = shape.triangle_up, at = anchor.below_bar, color = color.yellow.300, text = "Prism", title = "Long entry")
render.mark(go_s and flat and used[1] != -1, shape = shape.triangle_down, at = anchor.above_bar, color = color.violet.300, text = "Prism", title = "Short entry")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Trend Prism Follower", "Strategy · regression consensus and quality")
pr.row(t, 2, "Slopes up / down", str.format("{} / {}", pos, neg), pos > neg ? pr.BULL : pr.BEAR)
pr.row(t, 3, "Mean R squared", str.format("{:.2f}", quality), quality >= q_in ? color.yellow.300 : pr.LABEL)
pr.row(t, 4, "Stance", position.size > 0 ? "Long" : position.size < 0 ? "Short" : "Flat", color.white)
}Trendline Break
strategy · presets/strategy/trendline_break.qs · uses viz/premium
Trades breaks of automatically drawn swing trendlines. The two latest swing highs form a falling resistance line and the two latest swing lows a rising support line; a line is valid only while no close has crossed it since its second swing. A close through a valid line by a margin is the signal: through falling resistance to go long, through rising support to go short. Entry: close beyond the valid line by margin ATR. Exit: stop at the last swing on the other side, target reward R, trailing after 1R. Risk: 1 % per trade.
strategy "Trendline Break" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
max_objects = 40
}
/// Trades breaks of automatically drawn swing trendlines. The two latest swing highs form a falling
/// resistance line and the two latest swing lows a rising support line; a line is valid only while
/// no close has crossed it since its second swing. A close through a valid line by a margin is the
/// signal: through falling resistance to go long, through rising support to go short.
/// Entry: close beyond the valid line by `margin` ATR. Exit: stop at the last swing on the other
/// side, target `reward` R, trailing after 1R. Risk: 1 % per trade.
use "viz/premium" as pr
left = param.int(8, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right = param.int(4, title = "Swing bars right", min = 1, max = 30, group = "Swings")
margin = param.float(0.3, title = "Break margin (ATR)", min = 0, max = 3, step = 0.05, group = "Entry")
reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
a = ta.atr(30)
ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
persist h1x = na
persist h1y = na
persist h2x = na
persist h2y = na
persist l1x = na
persist l1y = na
persist l2x = na
persist l2y = na
persist res_ok = false
persist sup_ok = false
persist res_id = na
persist sup_id = na
if not math.is_na(ph) {
h1x := h2x
h1y := h2y
h2x := bar.index - right
h2y := ph
res_ok := not math.is_na(h1y) and h2y < h1y
if not math.is_na(res_id) { draw.segment.delete(res_id) }
res_id := res_ok ? draw.segment.create(h1x, h1y, h2x, h2y, color = color.rose.400, width = 1.5, ray = ray.right) : na
}
if not math.is_na(pl) {
l1x := l2x
l1y := l2y
l2x := bar.index - right
l2y := pl
sup_ok := not math.is_na(l1y) and l2y > l1y
if not math.is_na(sup_id) { draw.segment.delete(sup_id) }
sup_id := sup_ok ? draw.segment.create(l1x, l1y, l2x, l2y, color = color.emerald.400, width = 1.5, ray = ray.right) : na
}
res_y = res_ok ? h2y + (h2y - h1y) / math.max(h2x - h1x, 1) * (bar.index - h2x) : na
sup_y = sup_ok ? l2y + (l2y - l1y) / math.max(l2x - l1x, 1) * (bar.index - l2x) : na
brk_up = res_ok and close > res_y + margin * a
brk_dn = sup_ok and close < sup_y - margin * a
if brk_up { res_ok := false }
if brk_dn { sup_ok := false }
flat = position.size == 0
if flat and brk_up and not math.is_na(l2y) and close > l2y {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = l2y, target = close + (close - l2y) * reward, trail_offset = close - l2y, trail_trigger = close + (close - l2y))
}
if flat and brk_dn and not math.is_na(h2y) and close < h2y {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = h2y, target = close - (h2y - close) * reward, trail_offset = h2y - close, trail_trigger = close - (h2y - close))
}
risk.max_drawdown(25)
render.line(res_y, "Resistance line", color = color.translucent(color.rose.400, 60), width = 1, stroke = style.dotted)
render.line(sup_y, "Support line", color = color.translucent(color.emerald.400, 60), width = 1, stroke = style.dotted)
render.mark(brk_up, shape = shape.arrow_up, at = anchor.below_bar, color = color.emerald.300, text = "Break", title = "Resistance break")
render.mark(brk_dn, shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.300, text = "Break", title = "Support break")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Trendline Break", "Strategy · swing trendlines")
pr.row(t, 2, "Resistance", res_ok ? str.format("{:.2f}", res_y) : "none", pr.BEAR)
pr.row(t, 3, "Support", sup_ok ? str.format("{:.2f}", sup_y) : "none", pr.BULL)
}Turtle Pyramid Breakout
strategy · presets/strategy/turtle_pyramid_breakout.qs · uses viz/premium
The classic channel-breakout trend system with volatility units and pyramiding. A close beyond the entry_n-bar channel opens one unit; every further N move in favour (N = the 20-bar ATR at the first entry) adds another unit, up to four. Every unit shares one stop, three N behind the newest entry, so the open risk stays bounded while winners compound. The trade ends on a close beyond the shorter exit_n-bar channel on the other side. Entry: close above the prior entry_n-bar high (below the low for shorts); add a unit each add_n N, up to units. Exit: shared stop at newest entry -/+ stop_n N (3 by default, wider than the daily-bar original because hourly noise is larger relative to N), or a close through the exit_n-bar opposite channel. Risk: 0.5 % of equity per unit at its stop. Method: public channel-breakout rules with N-based units (Donchian channels, ATR).
strategy "Turtle Pyramid Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 0.5
pyramiding = 4
}
/// The classic channel-breakout trend system with volatility units and pyramiding. A close beyond
/// the `entry_n`-bar channel opens one unit; every further N move in favour (N = the 20-bar
/// ATR at the first entry) adds another unit, up to four. Every unit shares one stop, three N behind
/// the newest entry, so the open risk stays bounded while winners compound. The trade ends on a
/// close beyond the shorter `exit_n`-bar channel on the other side.
/// Entry: close above the prior `entry_n`-bar high (below the low for shorts); add a unit each
/// `add_n` N, up to `units`. Exit: shared stop at newest entry -/+ `stop_n` N (3 by default, wider
/// than the daily-bar original because hourly noise is larger relative to N), or a close through the
/// `exit_n`-bar opposite channel. Risk: 0.5 % of equity per unit at its stop.
/// Method: public channel-breakout rules with N-based units (Donchian channels, ATR).
use "viz/premium" as pr
entry_n = param.int(55, title = "Entry channel", min = 10, max = 500, group = "Channels")
exit_n = param.int(20, title = "Exit channel", min = 5, max = 300, group = "Channels")
units = param.int(4, title = "Maximum units", min = 1, max = 4, group = "Pyramid")
add_n = param.float(1.0, title = "Add every (N)", min = 0.1, max = 3, step = 0.1, group = "Pyramid")
stop_n = param.float(3.0, title = "Stop (N)", min = 0.5, max = 6, step = 0.1, group = "Risk")
n = ta.atr(20)
hh = ta.highest(high, entry_n)
ll = ta.lowest(low, entry_n)
xh = ta.highest(high, exit_n)
xl = ta.lowest(low, exit_n)
brk_up = close > hh[1]
brk_dn = close < ll[1]
persist count = 0
persist last_px = na
persist unit_n = na
persist stop = na
if position.size == 0 { count := 0 }
// shared stop for every open unit
fn restop_long(count: int, stop: float) {
exit.bracket("L1 stop", entry = "L1", stop = stop)
if count >= 2 { exit.bracket("L2 stop", entry = "L2", stop = stop) }
if count >= 3 { exit.bracket("L3 stop", entry = "L3", stop = stop) }
if count >= 4 { exit.bracket("L4 stop", entry = "L4", stop = stop) }
}
fn restop_short(count: int, stop: float) {
exit.bracket("S1 stop", entry = "S1", stop = stop)
if count >= 2 { exit.bracket("S2 stop", entry = "S2", stop = stop) }
if count >= 3 { exit.bracket("S3 stop", entry = "S3", stop = stop) }
if count >= 4 { exit.bracket("S4 stop", entry = "S4", stop = stop) }
}
flat = position.size == 0
if flat and brk_up {
unit_n := n
last_px := close
stop := close - stop_n * n
count := 1
order.market("L1", side.long)
restop_long(count, stop)
} else if flat and brk_dn {
unit_n := n
last_px := close
stop := close + stop_n * n
count := 1
order.market("S1", side.short)
restop_short(count, stop)
} else if position.size > 0 and count < units and close >= last_px + add_n * unit_n {
count := count + 1
last_px := close
stop := close - stop_n * unit_n
if count == 2 { order.market("L2", side.long) }
if count == 3 { order.market("L3", side.long) }
if count == 4 { order.market("L4", side.long) }
restop_long(count, stop)
} else if position.size < 0 and count < units and close <= last_px - add_n * unit_n {
count := count + 1
last_px := close
stop := close + stop_n * unit_n
if count == 2 { order.market("S2", side.short) }
if count == 3 { order.market("S3", side.short) }
if count == 4 { order.market("S4", side.short) }
restop_short(count, stop)
}
if position.size > 0 and close < xl[1] { exit.close_all() }
if position.size < 0 and close > xh[1] { exit.close_all() }
risk.max_drawdown(30)
up_p = render.line(hh, "Entry channel high", color = color.translucent(color.emerald.300, 45), width = 1)
dn_p = render.line(ll, "Entry channel low", color = color.translucent(color.rose.300, 45), width = 1)
render.band(up_p, dn_p, texture = "gradient", direction = "edges", upper_color = color.translucent(color.emerald.500, 88), lower_color = color.translucent(color.rose.500, 88), opacity = 1, title = "Channel")
render.line(xh, "Exit channel high", color = color.translucent(color.slate.300, 65), width = 1, stroke = style.dotted)
render.line(xl, "Exit channel low", color = color.translucent(color.slate.300, 65), width = 1, stroke = style.dotted)
render.step(position.size != 0 ? stop : na, "Shared stop", color = color.amber.300, width = 1.25, stroke = style.dashed)
render.mark(flat and brk_up, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "1", title = "First unit long")
render.mark(flat and brk_dn, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "1", title = "First unit short")
render.mark(not flat and count > count[1], shape = shape.circle, at = anchor.below_bar, color = color.amber.200, size = 0.45, title = "Unit added")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Turtle Pyramid Breakout", "Strategy · channel breakout, N units")
pr.row(t, 2, "Units open", str.format("{} / {}", position.size == 0 ? 0 : count, units), color.white)
pr.row(t, 3, "N (ATR 20)", str.format("{:.2f}", n), pr.LABEL)
pr.row(t, 4, "Shared stop", position.size != 0 ? str.format("{:.2f}", stop) : "Flat", pr.WARN)
}Vacuum Breakout
strategy · presets/strategy/vacuum_breakout.qs
Range breakouts taken only when the order book around the price is unusually thin (a liquidity vacuum: resting size within the band far below its own history), so there is little to stop the move. Stop at the other side of the half range, target a multiple of the risk. Needs the order book (L2): trades only on bars with a recorded book.
strategy "Vacuum Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Range breakouts taken only when the order book around the price is unusually thin (a liquidity
/// vacuum: resting size within the band far below its own history), so there is little to stop the
/// move. Stop at the other side of the half range, target a multiple of the risk.
/// Needs the order book (L2): trades only on bars with a recorded book.
pct = param.float(0.5, title = "Band around mid (%)", min = 0.02, max = 5, step = 0.01)
z_vac = param.float(-1.0, title = "Vacuum z-score", min = -4, max = 0, step = 0.1)
brk = param.int(20, title = "Breakout range (bars)", min = 3, max = 500)
rr = param.float(2.5, title = "Reward / risk", min = 0.5, max = 10, step = 0.1)
z = stat.zscore(math.log(math.max(book.liquidity(pct), 1e-12)), 200)
vac = z < z_vac
hi_n = ta.highest(high, brk)
lo_n = ta.lowest(low, brk)
hh = hi_n[1]
ll = lo_n[1]
mid = (hh + ll) / 2
flat = position.size == 0
if flat and vac and close > hh {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = mid, target = close + rr * (close - mid))
}
if flat and vac and close < ll {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = mid, target = close - rr * (mid - close))
}
risk.max_drawdown(25)
pu = render.line(hh, "Range high", color = color.translucent(color.violet.300, 40), width = 1)
pd = render.line(ll, "Range low", color = color.translucent(color.violet.300, 40), width = 1)
render.band(pu, pd, color = vac ? color.violet.500 : color.slate.600, opacity = 0.07, title = "Range")
render.background(vac ? color.translucent(color.violet.500, 90) : na)Value Area Rotation
strategy · presets/strategy/value_area_rotation.qs · uses viz/premium
Trades the auction's two states. While price stays inside the session value area, the market is in balance and moves from one edge tend to rotate back to the point of control (POC); when two closes are accepted outside value, balance has broken and the move tends to extend. The strategy fades edge tests in balance and follows acceptance out of it. Entry: rotation, a bar that pokes beyond VAH / VAL and closes back inside; breakout, the second consecutive close outside value. Exit: rotation targets the POC, breakouts trail; stops beyond the bar. Risk: 1 % per trade. Method: Steidlmayer market profile, volume at price.
strategy "Value Area Rotation" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trades the auction's two states. While price stays inside the session value area, the market is
/// in balance and moves from one edge tend to rotate back to the point of control (POC); when two
/// closes are accepted outside value, balance has broken and the move tends to extend. The strategy
/// fades edge tests in balance and follows acceptance out of it.
/// Entry: rotation, a bar that pokes beyond VAH / VAL and closes back inside; breakout, the second
/// consecutive close outside value. Exit: rotation targets the POC, breakouts trail; stops beyond
/// the bar. Risk: 1 % per trade. Method: Steidlmayer market profile, volume at price.
use "viz/premium" as pr
rows = param.int(40, title = "Profile rows", min = 10, max = 200, group = "Profile")
va = param.float(0.7, title = "Value area", min = 0.5, max = 0.95, step = 0.01, group = "Profile")
stop_k = param.float(1.5, title = "Stop (ATR)", min = 0.3, max = 6, step = 0.1, group = "Risk")
(poc, vah, val) = ta.volume_profile(rows, va, "session")
a = ta.atr(20)
inside = close <= vah and close >= val
rot_l = low < val and close > val and inside
rot_s = high > vah and close < vah and inside
acc_u = close > vah and close[1] > vah[1] and not (close[2] > vah[2])
acc_d = close < val and close[1] < val[1] and not (close[2] < val[2])
flat = position.size == 0
if flat and rot_l and poc - close > 0.5 * a {
order.market("RL", side.long)
exit.bracket("RL exit", entry = "RL", stop = low - stop_k * a * 0.5, target = poc)
}
if flat and rot_s and close - poc > 0.5 * a {
order.market("RS", side.short)
exit.bracket("RS exit", entry = "RS", stop = high + stop_k * a * 0.5, target = poc)
}
if flat and acc_u and not math.is_na(a) {
order.market("BL", side.long)
exit.bracket("BL exit", entry = "BL", stop = close - stop_k * a, trail_offset = stop_k * a, trail_trigger = close + stop_k * a)
}
if flat and acc_d and not math.is_na(a) {
order.market("BS", side.short)
exit.bracket("BS exit", entry = "BS", stop = close + stop_k * a, trail_offset = stop_k * a, trail_trigger = close - stop_k * a)
}
dom = time.day(time)
new_day = dom != dom[1]
if new_day and position.size != 0 and position.bars_held > 1 { exit.close_all() }
risk.max_drawdown(25)
p_h = render.step(vah, "Value area high", color = color.translucent(color.violet.300, 30), width = 1)
p_p = render.step(poc, "POC", color = color.amber.300, width = 2)
p_l = render.step(val, "Value area low", color = color.translucent(color.violet.300, 30), width = 1)
render.band(p_h, p_l, color = color.violet.500, opacity = 0.06, title = "Value area")
render.mark(rot_l or rot_s, shape = shape.circle, at = anchor.above_bar, color = color.violet.300, size = 0.4, title = "Edge rejection")
render.mark(acc_u or acc_d, shape = shape.diamond, at = anchor.below_bar, color = color.amber.300, size = 0.6, title = "Acceptance outside value")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Value Area Rotation", "Strategy · session auction")
pr.row(t, 2, "State", inside ? "Balance (rotate)" : "Imbalance (follow)", color.white)
pr.row(t, 3, "POC", str.format("{:.2f}", poc), color.amber.300)
}Variance Ratio Breakout
strategy · presets/strategy/variance_ratio_breakout.qs · uses viz/premium
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).
strategy "Variance Ratio Breakout" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// 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).
use "viz/premium" as pr
q = param.int(8, title = "Holding period (bars)", min = 2, max = 60, group = "Test")
vr_len = param.int(200, title = "Estimation window", min = 50, max = 3000, group = "Test")
vr_min = param.float(1.1, title = "Minimum variance ratio", min = 0.8, max = 3, step = 0.01, group = "Test")
kc_len = param.int(20, title = "Channel length", min = 5, max = 200, group = "Entry")
kc_mult = param.float(2.0, title = "Channel width (ATR)", min = 0.5, max = 5, step = 0.1, group = "Entry")
stop_k = param.float(1.8, title = "Stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
reward = param.float(2.5, title = "Target (R)", min = 0.5, max = 10, step = 0.1, group = "Risk")
vr = hft.variance_ratio(close, vr_len, q)
(kc_mid, kc_up, kc_lo) = ta.keltner(close, kc_len, kc_mult, kc_len)
a = ta.atr(20)
momentum = vr > vr_min
flat = position.size == 0
if flat and momentum and close > kc_up and close[1] <= kc_up[1] {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, target = close + stop_k * a * reward)
}
if flat and momentum and close < kc_lo and close[1] >= kc_lo[1] {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, target = close - stop_k * a * reward)
}
if position.size != 0 and position.bars_held >= 2 * q { exit.close_all() }
risk.max_drawdown(25)
pu = render.line(kc_up, "Channel high", color = color.translucent(color.amber.300, 40), width = 1)
pl = render.line(kc_lo, "Channel low", color = color.translucent(color.amber.300, 40), width = 1)
render.band(pu, pl, color = momentum ? color.amber.500 : color.slate.600, opacity = 0.06, title = "Channel")
render.line(kc_mid, "Channel mid", color = momentum ? color.amber.300 : color.slate.500, width = 1.5)
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Variance Ratio Breakout", "Strategy · Lo-MacKinlay filter")
pr.row(t, 2, str.format("VR({})", q), str.format("{:.3f}", vr), momentum ? pr.BULL : pr.LABEL)
pr.row(t, 3, "Breakouts", momentum ? "Traded" : "Ignored", momentum ? pr.BULL : pr.LABEL)
}Velocity Tracker Trend
strategy · presets/strategy/velocity_tracker_trend.qs · uses viz/premium
Trend following on a tracked velocity instead of a moving-average cross. An alpha-beta filter (the steady-state constant-velocity Kalman filter) estimates price level and velocity together. A trade starts when the velocity, in ATR per bar, passes a threshold while acceleration agrees, so the system joins moves that are speeding up rather than any drift. The stop trails at a multiple of the tracking error below the tracked level. Entry: velocity above v_in ATR per bar with positive smoothed acceleration and price inside the two-sigma envelope (no chasing shocks); mirror for shorts. Exit: trailing stop at the level minus k_err tracking errors (never loosened), or velocity back through zero. Risk: 1 % per trade at the initial stop.
strategy "Velocity Tracker Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Trend following on a tracked velocity instead of a moving-average cross. An alpha-beta filter
/// (the steady-state constant-velocity Kalman filter) estimates price level and velocity together.
/// A trade starts when the velocity, in ATR per bar, passes a threshold while acceleration agrees,
/// so the system joins moves that are speeding up rather than any drift. The stop trails at a
/// multiple of the tracking error below the tracked level.
/// Entry: velocity above `v_in` ATR per bar with positive smoothed acceleration and price inside
/// the two-sigma envelope (no chasing shocks); mirror for shorts. Exit: trailing stop at the level
/// minus `k_err` tracking errors (never loosened), or velocity back through zero. Risk: 1 % per
/// trade at the initial stop.
use "viz/premium" as pr
alpha = param.float(0.18, title = "Alpha (level gain)", min = 0.01, max = 0.9, step = 0.01, group = "Tracker")
beta = param.float(0.006, title = "Beta (velocity gain)", min = 0.0005, max = 0.5, step = 0.0005, group = "Tracker")
err_n = param.int(50, title = "Error window", min = 5, max = 1000, group = "Tracker")
v_in = param.float(0.08, title = "Entry velocity (ATR / bar)", min = 0.005, max = 1, step = 0.005, group = "Signals")
k_err = param.float(4.0, title = "Trailing stop (tracking errors)", min = 0.5, max = 10, step = 0.1, group = "Risk")
persist lvl = na
persist vel = 0.0
pred = math.is_na(lvl) ? close : lvl + vel
resid = close - pred
lvl := pred + alpha * resid
vel := vel + beta * resid
err = stat.stdev(resid, err_n)
a = ta.atr(14)
v = vel / math.max(a, 1e-12)
acc = ta.ema(v - v[1], 5)
calm = math.abs(close - lvl) < 2 * err
go_l = v > v_in and acc > 0 and calm
go_s = v < -v_in and acc < 0 and calm
persist used = 0
if v < 0 and used == 1 { used := 0 }
if v > 0 and used == -1 { used := 0 }
persist trail = na
flat = position.size == 0
if flat and go_l and used != 1 {
trail := lvl - k_err * err
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = trail)
used := 1
}
if flat and go_s and used != -1 {
trail := lvl + k_err * err
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = trail)
used := -1
}
if position.size > 0 {
trail := math.max(trail, lvl - k_err * err)
exit.bracket("L stop", entry = "L", stop = trail)
if v < 0 { exit.close("L") }
}
if position.size < 0 {
trail := math.min(trail, lvl + k_err * err)
exit.bracket("S stop", entry = "S", stop = trail)
if v > 0 { exit.close("S") }
}
risk.max_drawdown(25)
tone = color.ramp(math.clamp(v, -0.25, 0.25), -0.25, 0.25, "coolwarm")
u = render.line(lvl + err, "Envelope top", color = color.transparent, width = 0)
d = render.line(lvl - err, "Envelope bottom", color = color.transparent, width = 0)
render.band(u, d, color = color.ramp(math.clamp(v, -0.25, 0.25), -0.25, 0.25, "coolwarm", 65), opacity = 1, title = "Velocity envelope")
render.line(lvl, "Tracked level", color = tone, width = 2)
render.step(position.size != 0 ? trail : na, "Trailing stop", color = color.translucent(color.white, 30), width = 1, stroke = style.dashed)
render.mark(go_l and flat and used[1] != 1, shape = shape.triangle_up, at = anchor.below_bar, color = color.red.300, text = "v+", title = "Long entry")
render.mark(go_s and flat and used[1] != -1, shape = shape.triangle_down, at = anchor.above_bar, color = color.blue.300, text = "v-", title = "Short entry")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Velocity Tracker Trend", "Strategy · alpha-beta velocity")
pr.row(t, 2, "Velocity", str.format("{:+.3f} ATR / bar", v), v > 0 ? pr.BULL : pr.BEAR)
pr.row(t, 3, "Acceleration", acc > 0 ? "Speeding up" : "Slowing", acc > 0 ? pr.BULL : pr.BEAR)
pr.row(t, 4, "Trailing stop", position.size != 0 ? str.format("{:.2f}", trail) : "Flat", color.white)
}VIDYA Range Fade
strategy · presets/strategy/vidya_range_fade.qs
Fades rejections of the VIDYA bands while VIDYA is flat (a range), targeting VIDYA itself.
strategy "VIDYA Range Fade" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Fades rejections of the VIDYA bands while VIDYA is flat (a range), targeting VIDYA itself.
len = param.int(20, title = "Length", min = 2, max = 500)
mult = param.float(1.5, title = "Band (ATR)", min = 0.2, max = 10, step = 0.1)
stopk = param.float(1.0, title = "Stop beyond band (ATR)", min = 0.1, max = 10, step = 0.1)
v = ta.vidya(close, len, 9)
a = ta.atr(14)
flat_v = math.abs(v - v[5]) / math.max(a, 1e-12) < 0.5
up = v + mult * a
dn = v - mult * a
flat = position.size == 0
if flat and flat_v and high > up and close < up and not math.is_na(a) {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = up + stopk * a, target = v)
}
if flat and flat_v and low < dn and close > dn and not math.is_na(a) {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = dn - stopk * a, target = v)
}
risk.max_drawdown(25)
pu = render.line(up, "Upper", color = color.translucent(color.violet.300, 40), width = 1)
render.line(v, "VIDYA", color = color.amber.300, width = 1.5)
pd = render.line(dn, "Lower", color = color.translucent(color.violet.300, 40), width = 1)
render.band(pu, pd, color = color.violet.500, opacity = 0.06, title = "Band")Volatility-Targeted Trend
strategy · presets/strategy/volatility_target_trend.qs · uses viz/premium
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.
strategy "Volatility-Targeted Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = percent_equity
size = 95
pyramiding = 1
}
/// 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.
use "viz/premium" as pr
fast_q = param.float(0.01, title = "Fast Kalman noise ratio", min = 0.00001, max = 1, step = 0.0005, group = "Trend")
slow_q = param.float(0.0006, title = "Slow Kalman noise ratio", min = 0.000001, max = 0.5, step = 0.0001, group = "Trend")
vol_len = param.int(30, title = "Volatility window", min = 5, max = 500, group = "Volatility")
rank_len = param.int(500, title = "Percentile history", min = 50, max = 5000, group = "Volatility")
cap = param.float(80, title = "Volatility cap (percentile)", min = 30, max = 100, group = "Volatility")
dis_max = param.float(0.45, title = "Maximum disagreement", min = 0.05, max = 2, step = 0.05, group = "Volatility")
stop_k = param.float(4, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1, group = "Risk")
// process / measurement noise ratio sets the responsiveness (about EMA 20 and EMA 80 here)
f = ta.kalman(close, fast_q, 1)
s = ta.kalman(close, slow_q, 1)
yz = hft.yang_zhang(vol_len, 252)
pk = hft.parkinson(vol_len, 252)
gk = hft.garman_klass(vol_len, 252)
rs = hft.rogers_satchell(vol_len, 252)
cc = hft.realized_vol(close, vol_len, 252)
mean_v = (yz + pk + gk + rs + cc) / 5
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)
pct = stat.percent_rank(yz, rank_len)
// hysteresis: enter below the cap, leave only well above it, so the filter does not flicker
persist calm = false
if pct < cap and dis < dis_max { calm := true }
if pct > math.min(cap + 12, 100) or dis > dis_max * 1.4 { calm := false }
a = ta.atr(20)
// direction changes only once the lines separate by half an ATR
persist dir = 0
if f - s > 0.5 * a { dir := 1 }
if s - f > 0.5 * a { dir := -1 }
want = calm ? dir : 0
// one trade per direction signal: after a stop, wait for the next flip instead of re-entering
persist used = 0
if dir != dir[1] { used := 0 }
if want == 1 and position.side <= 0 and used != 1 {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a)
used := 1
}
if want == -1 and position.side >= 0 and used != -1 {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a)
used := -1
}
if want == 0 and position.size != 0 { exit.close_all() }
risk.max_drawdown(30)
pf = render.line(f, "Fast Kalman", color = calm ? (f > s ? color.emerald.300 : color.rose.300) : color.slate.500, width = 2)
ps = render.line(s, "Slow Kalman", color = color.translucent(color.slate.300, 40), width = 1.5)
render.band(pf, ps, color = color.translucent(f > s ? color.emerald.500 : color.rose.500, calm ? 85 : 97), opacity = 1, title = "Trend ribbon")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Volatility-Targeted Trend", "Strategy · Kalman trend, YZ volatility")
pr.row(t, 2, "Volatility percentile", str.format("{:.0f}", pct), pct < cap ? pr.BULL : pr.WARN)
pr.row(t, 3, "Estimator disagreement", str.format("{:.0%}", dis), dis < dis_max ? pr.BULL : pr.WARN)
pr.row(t, 4, "Stance", want > 0 ? "Long" : want < 0 ? "Short" : "Stand aside", color.white)
}Volume-Confirmed Trend
strategy · presets/strategy/volume_confirmed_trend.qs · uses viz/premium
Joins trends that volume confirms. Two things are measured: whether up bars carry the volume (the correlation of signed returns with volume) and whether the trend average is sloping. A cross of price over the average is taken only when the correlation agrees with its direction, which filters out drifts that happen on thin, one-sided volume. Entry: close crosses the Hull average with the return-volume correlation above (below) the level. Exit: opposite cross or ATR trailing stop. Risk: 1 % per trade. Method: rolling Pearson correlation of log returns and log volume.
strategy "Volume-Confirmed Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Joins trends that volume confirms. Two things are measured: whether up bars carry the volume
/// (the correlation of signed returns with volume) and whether the trend average is sloping. A cross
/// of price over the average is taken only when the correlation agrees with its direction, which
/// filters out drifts that happen on thin, one-sided volume.
/// Entry: close crosses the Hull average with the return-volume correlation above (below) the level.
/// Exit: opposite cross or ATR trailing stop. Risk: 1 % per trade.
/// Method: rolling Pearson correlation of log returns and log volume.
use "viz/premium" as pr
hull_l = param.int(55, title = "Hull average", min = 5, max = 500, group = "Trend")
c_len = param.int(60, title = "Correlation window", min = 10, max = 1000, group = "Volume")
c_min = param.float(0.08, title = "Confirmation level", min = 0, max = 0.8, step = 0.01, group = "Volume")
stop_k = param.float(2.5, title = "Trailing stop (ATR)", min = 0.5, max = 8, step = 0.1, group = "Risk")
h = ta.hull(close, hull_l)
r = math.log(close / close[1])
corr = stat.corr(r, math.log(math.max(volume, 1)), c_len)
a = ta.atr(20)
up_x = ta.cross_up(close, h)
dn_x = ta.cross_down(close, h)
long_sig = up_x and corr > c_min and h > h[1]
short_sig = dn_x and corr < -c_min and h < h[1]
if position.size == 0 and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = close - stop_k * a, trail_offset = stop_k * a, trail_trigger = close)
}
if position.size == 0 and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = close + stop_k * a, trail_offset = stop_k * a, trail_trigger = close)
}
if (position.side > 0 and dn_x) or (position.side < 0 and up_x) { exit.close_all() }
risk.max_drawdown(25)
render.line(h, "Hull average", color = corr > c_min ? color.emerald.300 : corr < -c_min ? color.rose.300 : color.slate.500, width = 2)
render.mark(long_sig, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, size = 0.7, title = "Confirmed up")
render.mark(short_sig, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.7, title = "Confirmed down")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Volume-Confirmed Trend", "Strategy · return-volume correlation")
pr.row(t, 2, "Correlation", str.format("{:+.2f}", corr), corr > c_min ? pr.BULL : corr < -c_min ? pr.BEAR : pr.LABEL)
pr.row(t, 3, "Volume", corr > c_min ? "Buyers lead" : corr < -c_min ? "Sellers lead" : "Neutral", color.white)
}VWAP Deviation Reversion
strategy · presets/strategy/vwap_deviation_reversion.qs · uses viz/premium
Fades stretches away from the session VWAP measured in volume-weighted standard deviations, but only on low relative volume: a stretch made without participation tends to snap back, a stretch on heavy volume is often the start of a trend and is left alone. Entry: close beyond the 2-sigma VWAP band, relative volume below the limit, and a reversal bar. Exit: target at VWAP, stop beyond the 3-sigma band (at least 1 ATR away), flat at the session change. Risk: 1 % per trade.
strategy "VWAP Deviation Reversion" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
max_leverage = 1
size = 1
pyramiding = 1
}
/// Fades stretches away from the session VWAP measured in volume-weighted standard deviations, but
/// only on low relative volume: a stretch made without participation tends to snap back, a stretch
/// on heavy volume is often the start of a trend and is left alone.
/// Entry: close beyond the 2-sigma VWAP band, relative volume below the limit, and a reversal bar.
/// Exit: target at VWAP, stop beyond the 3-sigma band (at least 1 ATR away), flat at the session change.
/// Risk: 1 % per trade.
use "viz/premium" as pr
k_in = param.float(2.0, title = "Entry band (sigma)", min = 0.5, max = 4, step = 0.1, group = "Bands")
k_stop = param.float(3.2, title = "Stop band (sigma)", min = 1, max = 6, step = 0.1, group = "Bands")
rv_max = param.float(1.2, title = "Maximum relative volume", min = 0.3, max = 5, step = 0.1, group = "Filter")
rv_len = param.int(48, title = "Relative volume window", min = 5, max = 1000, group = "Filter")
(vw, up1, dn1) = ta.vwap(bar.typical, "day", 1)
sdv = up1 - vw
rv = volume / math.max(ta.sma(volume, rv_len), 1e-12)
quiet = rv < rv_max
a = ta.atr(20)
long_sig = close[1] < vw[1] - k_in * sdv[1] and close > open and quiet
short_sig = close[1] > vw[1] + k_in * sdv[1] and close < open and quiet
flat = position.size == 0
if flat and long_sig {
order.market("L", side.long)
exit.bracket("L exit", entry = "L", stop = math.min(vw - k_stop * sdv, close - a), target = vw)
}
if flat and short_sig {
order.market("S", side.short)
exit.bracket("S exit", entry = "S", stop = math.max(vw + k_stop * sdv, close + a), target = vw)
}
dom = time.day(time)
if dom != dom[1] and position.size != 0 { exit.close_all() }
risk.max_drawdown(25)
pu = render.line(vw + k_in * sdv, "Upper band", color = color.translucent(color.rose.300, 45), width = 1)
pv = render.line(vw, "VWAP", color = color.amber.300, width = 2)
pd = render.line(vw - k_in * sdv, "Lower band", color = color.translucent(color.emerald.300, 45), width = 1)
render.band(pu, pv, texture = "gradient", direction = "edges", upper_color = color.rose.500, lower_color = color.slate.900, opacity = 0.16, title = "Rich zone")
render.band(pv, pd, texture = "gradient", direction = "edges", upper_color = color.slate.900, lower_color = color.emerald.500, opacity = 0.16, title = "Cheap zone")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "VWAP Deviation Reversion", "Strategy · quiet stretches")
pr.row(t, 2, "Deviation", str.format("{:+.2f} sigma", (close - vw) / math.max(sdv, 1e-12)), color.white)
pr.row(t, 3, "Relative volume", str.format("{:.2f}", rv), quiet ? pr.BULL : pr.WARN)
}Zero-Lag Trend
strategy · presets/strategy/zero_lag_trend.qs
Trades crosses of a zero-lag EMA over a regular EMA in the direction of a slow EMA, with an ATR stop and an exit on the opposite cross.
strategy "Zero-Lag Trend" {
overlay = true
capital = 100000
commission = 0.05
slippage = 1
sizing = risk_percent
size = 1
max_leverage = 1
}
/// Trades crosses of a zero-lag EMA over a regular EMA in the direction of a slow EMA, with an ATR
/// stop and an exit on the opposite cross.
len = param.int(34, title = "Length", min = 2, max = 500)
trend = param.int(200, title = "Trend filter", min = 10, max = 2000)
stopk = param.float(2.5, title = "Stop (ATR)", min = 0.5, max = 10, step = 0.1)
z = ta.zero_lag(close, len)
e = ta.ema(close, len)
t = ta.ema(close, trend)
a = ta.atr(14)
flat = position.size == 0
if flat and ta.cross_up(z, e) and close > t and not math.is_na(a) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - stopk * a)
}
if flat and ta.cross_down(z, e) and close < t and not math.is_na(a) {
order.market("S", side.short)
exit.bracket("S stop", entry = "S", stop = close + stopk * a)
}
if position.size > 0 and ta.cross_down(z, e) { exit.close_all() }
if position.size < 0 and ta.cross_up(z, e) { exit.close_all() }
risk.max_drawdown(25)
render.line(z, "Zero-lag", color = color.cyan.300, width = 1.5)
render.line(t, "Trend filter", color = color.translucent(color.slate.300, 40), width = 1)