Indicator · Pine Script v5 · Own pane

Qullamaggie Setups - Breakout / EP / Parabolic

by axidzz· code credited to YourNameHere

This indicator identifies Qullamaggie-style momentum trading setups, including Breakout, Episodic Pivot (EP), and Parabolic reversals.

View on GitHub

GitHub stars24on the repository
Pine versionv5runs as is
Lines of code551
Settings40inputs
LicenceMIT
Last update2 months ago7 Aug 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

551 lines of Pine Script v5 from Qullamaggie_Setups.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

Qullamaggie_Setups.pine
// This source code is subject to the terms of the Mozilla Public License 2.0
// © YourNameHere
//@version=5
// overlay=false + force_overlay=true on every existing plot/drawing call below:
// this lets the script keep drawing on the main price chart (boxes, labels,
// table, setup markers) exactly as before, while getting its own separate pane
// for a plain volume histogram at the bottom - like TradingView's own built-in
// Volume indicator, just folded into this single indicator slot instead of a
// second one. (Reverted 2026-07-30 from an on-chart rescaled-overlay attempt
// that turned out too fiddly to get right without being able to test-render
// Pine directly - this plain separate-pane version is simpler and known-good.)
indicator("Qullamaggie Setups - Breakout / EP / Parabolic", shorttitle="QM Setups", overlay=false,
     format=format.volume, max_labels_count=500, max_bars_back=500, max_boxes_count=500)

// =====================================================================================
// Implements the 3 setups described by Kristjan Kullamägi ("Qullamaggie"):
//   1. Breakout        -> big prior move, tight consolidation, breakout on volume
//   2. Episodic Pivot   -> 10%+ gap up on a volume surge, stock not already extended
//   3. Parabolic Short/Long -> extreme short-term move + consecutive days + stretch from MA
//
// Every setup also outputs a plain plot() (1/0) so you can add this indicator in
// TradingView's Stock Screener and sort/filter a watchlist by these columns.
// =====================================================================================

// ---------------------------------------------------------------------------
// GENERAL / LIQUIDITY FILTERS
// ---------------------------------------------------------------------------
grpGen = "General / Liquidity"
adrLen        = input.int(20, "ADR% Length", group = grpGen)
minADR        = input.float(3.0, "Min ADR % (avg daily range)", group = grpGen, tooltip = "ADR% = SMA(High/Low, len)*100 - 100. Qullamaggie generally wants volatile, tradeable names.")
volAvgLen     = input.int(20, "Volume Avg Length", group = grpGen)
minDollarVolM = input.float(5.0, "Min Avg Dollar Volume ($M)", group = grpGen,
     tooltip = "In millions - e.g. 5 = $5,000,000. Kept in millions (not the raw dollar figure) since the raw"
     + " number was bloating the indicator's auto-generated title string at the bottom of the chart.")
minDollarVol  = minDollarVolM * 1000000
minPrice      = input.float(2.0, "Min Price", group = grpGen)

// ---------------------------------------------------------------------------
// BREAKOUT SETUP
// ---------------------------------------------------------------------------
grpBO = "Breakout Setup"
boMoveLookback   = input.int(60, "Prior-Move Lookback (bars)", group = grpBO, tooltip = "~60 bars = ~3 months on daily chart")
boMinMovePct     = input.float(30, "Min Prior Move %", group = grpBO)
boConsolLen      = input.int(15, "Consolidation Length (bars, excludes today)", group = grpBO)
boMaxRangePct    = input.float(15, "Max Consolidation Range %", group = grpBO, tooltip = "High-to-low range of the consolidation window, as % of the low. Keeps consolidation 'tight'. (Was 35 - a dedicated Qullamaggie-methodology script uses <10% VCP tightness over a 5-day window; adjusted to 15% for our longer 15-day window, since longer windows accumulate more range even in a genuinely orderly base.)")
boVolExpMult     = input.float(1.5, "Breakout Volume >= x * Avg Volume", group = grpBO)
boRequireAboveEMA = input.bool(true, "Require Close > EMA20 on breakout bar", group = grpBO)
boRequireAbove50MA = input.bool(true, "Require Close > 50-day SMA (Kris's stated hard rule: 'never buys below 50MA')", group = grpBO)

// ---------------------------------------------------------------------------
// EPISODIC PIVOT (EP) SETUP
// ---------------------------------------------------------------------------
grpEP = "Episodic Pivot Setup"
epMinGapPct      = input.float(10, "Min Gap Up %", group = grpEP)
epMinVolMult     = input.float(3.0, "Volume >= x * Avg Volume", group = grpEP)
epNoRunLookback  = input.int(120, "'No Prior Run' Lookback (bars, ~6mo)", group = grpEP)
epMaxPriorRunPct = input.float(20, "Max Prior Run % (skip if already extended)", group = grpEP, tooltip = "Was 40 - sources describe the ideal pre-EP base as 'flat for 3-6 months prior'; 40% clearly doesn't qualify as flat.")

// ---------------------------------------------------------------------------
// PARABOLIC SHORT / LONG SETUP
// ---------------------------------------------------------------------------
grpPB = "Parabolic Short/Long Setup"
pbLookback      = input.int(10, "Parabolic Move Lookback (bars)", group = grpPB)
pbMinMovePct    = input.float(50, "Min Parabolic Move % (up for short, down for long)", group = grpPB)
pbMinStreak     = input.int(4, "Min Consecutive Up/Down Closes", group = grpPB)
pbMAlen         = input.int(20, "Stretch Reference MA Length", group = grpPB)
pbMinStretchPct = input.float(30, "Min % Stretch From MA", group = grpPB)
pbLongLookbackBars = input.int(5, "Bars After Crash To Look For Long Reversal", group = grpPB)

// =====================================================================================
// CALCULATIONS
// =====================================================================================

// --- Liquidity / tradeability filter ---
adrPct   = ta.sma(high / low, adrLen) * 100 - 100
avgVol   = ta.sma(volume, volAvgLen)
// BUGFIX (same one found in the Python Stage-1 scanner): dollar volume must be
// the average of (daily volume * that day's price), NOT (avg volume * today's
// price) - the latter mixes an older, possibly much-lower price regime's
// volume with today's price. For a stock that just spiked, this can make an
// actually-thin stock look like it clears the liquidity bar when its real
// day-to-day dollar volume never has.
dollarVol = ta.sma(volume * close, volAvgLen)
liquidityOK = adrPct >= minADR and dollarVol >= minDollarVol and close >= minPrice

// --- Moving averages Qullamaggie references (10/20/50) ---
// Kris has stated directly that price should "smoothly surf the 10 and 20
// ema" - so both the boolean signals AND the 0-100 scoring buckets below
// use these same EMA10/EMA20 (not a separate SMA pair). 50/200 stay SMA,
// which matches the standard convention for the slower trend/regime MAs.
ema10 = ta.ema(close, 10)
ema20 = ta.ema(close, 20)
sma50 = ta.sma(close, 50)
sma200 = ta.sma(close, 200)

// ---------------------------------------------------------------------------
// 1. BREAKOUT
// ---------------------------------------------------------------------------
// Prior big move: range expansion over the lookback window (excluding the live consolidation)
priorWindowHigh = ta.highest(high, boMoveLookback)
priorWindowLow  = ta.lowest(low, boMoveLookback)
priorMovePct    = (priorWindowHigh - priorWindowLow) / priorWindowLow * 100
madeBigMove     = priorMovePct >= boMinMovePct

// Consolidation = the N bars before today (today is the potential breakout bar)
consolHigh = ta.highest(high[1], boConsolLen)
consolLow  = ta.lowest(low[1], boConsolLen)
consolRangePct = (consolHigh - consolLow) / consolLow * 100
tightConsolidation = consolRangePct <= boMaxRangePct

volExpansion = volume >= avgVol * boVolExpMult
breaksHigh   = close > consolHigh and close > close[1]
emaFilter    = not boRequireAboveEMA or close > ema20
sma50Filter  = not boRequireAbove50MA or close > sma50

breakoutSetup = liquidityOK and madeBigMove and tightConsolidation and volExpansion and breaksHigh and emaFilter and sma50Filter

// ---------------------------------------------------------------------------
// 2LYNCH CONSOLIDATION-QUALITY CHECKLIST (Stockbee/Pradeep Bonde-derived)
// Same six checks as the Python Stage-1 scanner's check_2lynch(), so a name
// that scores well in Stage 1 can be visually confirmed here in Stage 2.
//   2 = not up two days in a row already, going into today
//   L = linearity of the prior move (efficiency ratio, not just a jagged spike)
//   Y = young trend (breakout is early in the base's life, not aged/extended)
//   N = narrow-range or down day right before the trigger (coiling, not thrusting)
//   C = consolidation quality (shallow, orderly, <=N sharp down days, volume dry-up)
//   H = close near the high of the trigger bar
// ---------------------------------------------------------------------------
grp2L = "2LYNCH Quality Checklist"
lynchEfficiencyMin = input.float(0.35, "L: Min Efficiency Ratio (linearity)", group = grp2L, tooltip = "Kaufman efficiency ratio: net move / sum of daily |moves| over the breakout lookback. Higher = cleaner trend.")
lynchYoungMaxBars   = input.int(90, "Y: Max Bars Since Below 50-day SMA (young trend)", group = grp2L)
lynchDownDayPct     = input.float(4.0, "C: Sharp Down Day Threshold %", group = grp2L)
lynchMaxDownDays    = input.int(1, "C: Max Sharp Down Days Allowed in Base", group = grp2L)
lynchCloseRangeMin  = input.float(0.7, "H: Min Close Position in Bar's Range (0-1)", group = grp2L)
requireStrict       = input.bool(false, "Require Breakout to also pass Strict 2LYNCH", group = grp2L, tooltip = "Mirrors the Python scanner's --strict mode: gates the Breakout signal on lynchCount >= the threshold below.")
strictMinCount      = input.int(4, "Strict: Min 2LYNCH Count Required (of 6)", group = grp2L)

// "2" - not already up two days in a row going into today
prev1Up = close[1] > close[2]
prev2Up = close[2] > close[3]
lynch2 = not (prev1Up and prev2Up)

// "L" - linearity of the prior move (Kaufman efficiency ratio over the breakout lookback)
netMove = math.abs(close - close[boMoveLookback])
pathSum = 0.0
for i = 0 to boMoveLookback - 1
    pathSum += math.abs(close[i] - close[i + 1])
efficiency = pathSum > 0 ? netMove / pathSum : 0.0
lynchL = efficiency >= lynchEfficiencyMin

// "Y" - young trend: bars since price last closed below its 50-day SMA
belowSma50 = close < sma50
barsSinceBelow50 = ta.barssince(belowSma50)
lynchY = na(barsSinceBelow50) or barsSinceBelow50 <= lynchYoungMaxBars

// "N" - narrow-range or down day immediately before the trigger bar
barRange  = high - low
avgRange  = ta.sma(barRange, 20)
narrowPrev = barRange[1] < avgRange[1]
negPrev    = close[1] < close[2]
lynchN = narrowPrev or negPrev

// "C" - consolidation quality: <=N sharp down days in the base, plus volume dry-up
pctChg = (close - close[1]) / close[1] * 100
down4Count = 0
for i = 1 to boConsolLen
    if pctChg[i] <= -lynchDownDayPct
        down4Count += 1
volConsol    = ta.sma(volume[1], boConsolLen)
volPriorMove = ta.sma(volume[1], boMoveLookback)
lynchC = down4Count <= lynchMaxDownDays and volConsol < volPriorMove

// "H" - close near the high of the current bar
barSpan  = high - low
closePos = barSpan > 0 ? (close - low) / barSpan : 0.0
lynchH = closePos >= lynchCloseRangeMin

lynchCount = (lynch2 ? 1 : 0) + (lynchL ? 1 : 0) + (lynchY ? 1 : 0) + (lynchN ? 1 : 0) + (lynchC ? 1 : 0) + (lynchH ? 1 : 0)
lynchFlags = (lynch2 ? "2" : "") + (lynchL ? "L" : "") + (lynchY ? "Y" : "") + (lynchN ? "N" : "") + (lynchC ? "C" : "") + (lynchH ? "H" : "")

// Optional strict gate on the Breakout signal, mirroring the Python --strict flag
breakoutConfirmed = breakoutSetup and (not requireStrict or lynchCount >= strictMinCount)

// ---------------------------------------------------------------------------
// 2. EPISODIC PIVOT
// ---------------------------------------------------------------------------
gapPct = (open - close[1]) / close[1] * 100
priorLow6mo = ta.lowest(low[1], epNoRunLookback)
priorRunPct = (close[1] - priorLow6mo) / priorLow6mo * 100
notAlreadyExtended = priorRunPct <= epMaxPriorRunPct

epSetup = liquidityOK and gapPct >= epMinGapPct and volume >= avgVol * epMinVolMult and notAlreadyExtended

// ---------------------------------------------------------------------------
// 3. PARABOLIC SHORT / LONG
// ---------------------------------------------------------------------------
// consecutive up/down closes
var int upStreak = 0
var int downStreak = 0
upStreak   := close > close[1] ? upStreak + 1 : 0
downStreak := close < close[1] ? downStreak + 1 : 0

paraMoveUpPct   = (close - close[pbLookback]) / close[pbLookback] * 100
paraMoveDownPct = (close[pbLookback] - close) / close[pbLookback] * 100

pbMA = ta.sma(close, pbMAlen)
stretchAboveMAPct = (close - pbMA) / pbMA * 100
stretchBelowMAPct = (pbMA - close) / pbMA * 100

paraShortSetup = liquidityOK and paraMoveUpPct >= pbMinMovePct and upStreak >= pbMinStreak and stretchAboveMAPct >= pbMinStretchPct

// crash = the mirror condition (big fast drop w/ consecutive down closes) -> feeds the long setup
crashDetected = liquidityOK and paraMoveDownPct >= pbMinMovePct and downStreak >= pbMinStreak and stretchBelowMAPct >= pbMinStretchPct

var int barsSinceCrash = na
barsSinceCrash := crashDetected ? 0 : nz(barsSinceCrash[1]) + 1
recentCrash = barsSinceCrash <= pbLongLookbackBars

paraLongSetup = recentCrash and close > open and close > close[1] and liquidityOK

// =====================================================================================
// 0-100 SCORING SYSTEM
// Mirrors the Python Stage-1 scanner's score_htf_breakout() / score_episodic_pivot() /
// score_parabolic_short() exactly - same tier thresholds, same point values - so a
// ticker's score here should match what Stage 1 reports for the same bar. Unlike the
// pass/fail boolean setups above, this surfaces "getting there" candidates missing
// 1-2 criteria, not just fully-confirmed ones. Badge tiers: SETUP! (80-100) / WATCH
// (60-79) / Monitor (40-59) / - (0-39), same cutoffs as the Python BADGE_TIERS list.
// =====================================================================================

// --- shared raw inputs the score buckets are built from ---
ret1mPct   = (close - close[21]) / close[21] * 100   // ~1 month
ret3mPct   = (close - close[63]) / close[63] * 100   // ~3 months
dist10maSigned = (close - ema10) / ema10 * 100        // signed: + above, - below (Parabolic uses signed)
dist10maAbs = math.abs(dist10maSigned)                // unsigned distance (Breakout MA-surfing uses abs)
dist20maAbs = math.abs((close - ema20) / ema20 * 100)
high20d    = ta.highest(high[1], 20)                  // matches Python's h.shift(1).rolling(20).max()
high52w    = ta.highest(high, 252)
pct52wHigh = close / high52w * 100
pct20dHigh = close / high20d * 100
volMult    = volume / avgVol

// --- Breakout: momentum / trend position / MA-surfing / volume-extension ---
// (each sub-tier below is its own line - no line-continuation, avoids the Pine parser error)
bo_mom1m = ret1mPct >= 40 ? 20 : ret1mPct >= 25 ? 15 : ret1mPct >= 15 ? 10 : ret1mPct >= 5 ? 5 : 0
bo_mom3m = ret3mPct >= 80 ? 15 : ret3mPct >= 50 ? 10 : ret3mPct >= 30 ? 5 : 0
bo_momentum = bo_mom1m + bo_mom3m
// Redesigned 2026-07-30: previously flat "close > EMA10" / "close > EMA20" points
// regardless of trend strength. Now requires EMA10 to actually be RISING (slope
// confirmation, mirrors Minervini's approach) and the full MA stack in bullish
// order (EMA10 > EMA20 > SMA50, mirrors Minervini's 50>150>200 stack requirement).
// Same 20-point weight as before, just requiring real trend confirmation to earn it.
ema10Rising = ema10 > ema10[5]
maStacked = ema10 > ema20 and ema20 > sma50
bo_trendPosition = (ema10Rising ? 10 : 0) + (maStacked ? 10 : 0)
bo_surf10 = dist10maAbs <= 2 ? 20 : dist10maAbs <= 4 ? 15 : dist10maAbs <= 7 ? 10 : 0
bo_surf20 = dist20maAbs <= 5 ? 5 : 0
bo_maSurfing = bo_surf10 + bo_surf20
bo_volMultPts = volMult >= 2 ? 10 : volMult >= 1.3 ? 5 : 0
bo_pct52wPts = pct52wHigh >= 90 ? 10 : pct52wHigh >= 75 ? 5 : 0
bo_volExt = bo_volMultPts + bo_pct52wPts
bo_scoreRaw = bo_momentum + bo_trendPosition + bo_maSurfing + bo_volExt

// --- Episodic Pivot: gap size / volume surge / prior-base flatness / breakout-through-20d-high ---
ep_gapMove  = gapPct >= 20 ? 40 : gapPct >= 10 ? 30 : gapPct >= 5 ? 15 : 0
ep_volSurge = volMult >= 3 ? 25 : volMult >= 2 ? 20 : volMult >= 1.5 ? 10 : 0
// Bug fix (2026-07-30): was checking signed ret3mPct - a deep prior CRASH
// (e.g. ret3mPct = -73%) satisfied "<= 10" just as well as genuine flatness
// (+5%), incorrectly awarding full points to already-collapsed stocks.
// "Flat for 3-6 months" means low MAGNITUDE of movement either direction.
ret3mAbs = math.abs(ret3mPct)
ep_priorBase = ret3mAbs <= 10 ? 20 : ret3mAbs <= 25 ? 15 : ret3mAbs <= 40 ? 10 : 0
ep_breakoutPts = close > high20d ? 15 : (pct20dHigh >= 95 ? 10 : 0)
ep_scoreRaw = ep_gapMove + ep_volSurge + ep_priorBase + ep_breakoutPts

// --- Parabolic Short: massive gain / consecutive up days / extension above MA / volatility (ADR%) ---
pb_extVal = dist10maSigned > 0 ? dist10maSigned : 0   // only credit extension when actually above the MA
pb_massiveGain = ret1mPct >= 200 ? 35 : ret1mPct >= 100 ? 25 : ret1mPct >= 60 ? 15 : ret1mPct >= 40 ? 10 : 0
pb_consecUp    = upStreak >= 7 ? 25 : upStreak >= 5 ? 20 : upStreak >= 3 ? 10 : 0
pb_extension   = pb_extVal >= 40 ? 25 : pb_extVal >= 25 ? 20 : pb_extVal >= 15 ? 10 : 0
pb_volatility  = adrPct >= 10 ? 15 : adrPct >= 7 ? 10 : adrPct >= 5 ? 5 : 0
pshort_scoreRaw = pb_massiveGain + pb_consecUp + pb_extension + pb_volatility

// --- Same gating as Python: liquidity gate zeroes all three scores; the 50MA hard
// rule (Breakout-only) additionally zeroes just the Breakout score. ---
bo_scoreGated = sma50Filter ? bo_scoreRaw : 0
htfScore    = liquidityOK ? bo_scoreGated : 0
epScore     = liquidityOK ? ep_scoreRaw : 0
pshortScore = liquidityOK ? pshort_scoreRaw : 0

// --- Debug: Episodic Pivot raw inputs, Data Window only (not plotted on any pane) ---
// Lets you hover any historical bar and read off exactly what gapPct/volMult/priorRunPct
// were on that day, and which of the epSetup boolean's 4 gates passed - useful for cases
// where epScore (0-100, drives the dashboard's "Recently Triggered" table) crossed 80 but
// epSetup (the strict boolean that draws the "EP" chart marker) never fired, since the two
// use different thresholds and priorRunPct/notAlreadyExtended isn't part of epScore at all.
plot(gapPct,                title="Debug: Gap %",                display=display.data_window)
plot(volume / avgVol,       title="Debug: Vol Mult (vs avg)",    display=display.data_window)
plot(priorRunPct,           title="Debug: Prior Run % (vs 6mo low)", display=display.data_window)
plot(liquidityOK       ? 1 : 0, title="Debug: gate - liquidityOK",       display=display.data_window)
plot(gapPct >= epMinGapPct ? 1 : 0, title="Debug: gate - gap>=min",      display=display.data_window)
plot(volume >= avgVol * epMinVolMult ? 1 : 0, title="Debug: gate - vol>=min", display=display.data_window)
plot(notAlreadyExtended ? 1 : 0, title="Debug: gate - notAlreadyExtended", display=display.data_window)
plot(epSetup            ? 1 : 0, title="Debug: epSetup (all 4 gates)",   display=display.data_window)
plot(epScore,                title="Debug: epScore (0-100)",      display=display.data_window)

badgeText(score) =>
    score >= 80 ? "SETUP!" : score >= 60 ? "WATCH" : score >= 40 ? "Monitor" : "-"
badgeColor(score) =>
    score >= 80 ? color.lime : score >= 60 ? color.yellow : score >= 40 ? color.orange : color.gray

// --- Volume pane (plain, separate pane - like TradingView's own built-in
// Volume indicator, just folded into this one indicator slot) ---
// No force_overlay here (unlike everything else in this script) - this is the
// one plot meant to land in the new separate pane created by overlay=false
// above, not on the main price chart.
volBarColor = close >= open ? color.new(color.teal, 0) : color.new(color.red, 0)
plot(volume, title="Volume", style=plot.style_columns, color=volBarColor, format=format.volume)

// =====================================================================================
// PLOTTING - chart shapes
// =====================================================================================
plotshape(breakoutConfirmed, title="Breakout", style=shape.triangleup, location=location.belowbar, color=color.new(color.lime, 0), size=size.small, text="BO", force_overlay=true)
plotshape(epSetup,       title="Episodic Pivot", style=shape.labelup, location=location.belowbar, color=color.new(color.blue, 0), size=size.small, text="EP", textcolor=color.white, force_overlay=true)
plotshape(paraShortSetup, title="Parabolic Short", style=shape.triangledown, location=location.abovebar, color=color.new(color.red, 0), size=size.small, text="P-SHORT", force_overlay=true)
plotshape(paraLongSetup,  title="Parabolic Long", style=shape.triangleup, location=location.belowbar, color=color.new(color.orange, 0), size=size.small, text="P-LONG", force_overlay=true)

plot(ema10, "EMA10", color=color.new(color.yellow, 40), force_overlay=true)
plot(ema20, "EMA20", color=color.new(color.aqua, 40), force_overlay=true)
plot(sma50, "SMA50", color=color.new(color.gray, 40), force_overlay=true)
plot(sma200, "SMA200", color=color.new(color.white, 30), force_overlay=true)

// =====================================================================================
// LOOKBACK WINDOW MARKERS
// Shows exactly which bars each metric is being calculated over, directly on
// the chart, so you don't have to guess or hover the Data Window to find it.
// Pine can't do real mouse-hover interactivity (no such event model exists
// for indicators), so this is the practical alternative: always-visible
// markers you can toggle on/off, in either of two styles:
//   Box     = shaded fill over the whole window (like a highlighter)
//   Bracket = edge lines only, at the window's left/right boundaries (less clutter)
// Three windows, one shared style setting:
//   1. Consolidation (boConsolLen bars) - what Breakout/2LYNCH-"C" evaluates
//   2. Parabolic lookback (pbLookback bars) - what the "Down move %"/"Down
//      streak"/"Stretch below MA" table rows are computed over
//   3. 2LYNCH Linearity (boMoveLookback bars) - what the "L" efficiency-ratio
//      check is computed over
// Confirmed (historical, per-breakout) zones stay as permanent boxes always -
// they're a record of a past event, not a "where do I look right now" aid,
// so the Box/Bracket toggle doesn't apply to them.
// =====================================================================================
grpMarkers = "Lookback Window Markers"
markerStyle = input.string("Box", "Marker Style (applies to all live windows below)", options=["Box", "Bracket"],
     group = grpMarkers, tooltip = "Box = shaded fill. Bracket = edge lines only, less visual clutter.")
showConfirmedZones = input.bool(true, "Show Confirmed Breakout Zones (Consolidation, historical)", group = grpMarkers,
     tooltip = "Permanently marks the consolidation that preceded each confirmed Breakout signal. Always drawn as a box.")
showConsolWindow = input.bool(false, "Show Consolidation Window (live)", group = grpMarkers)
showParaWindow = input.bool(false, "Show Parabolic Lookback Window (live)", group = grpMarkers)
showLinearityWindow = input.bool(false, "Show 2LYNCH Linearity Window (live)", group = grpMarkers)
zoneTightColor      = input.color(color.new(color.aqua, 85), "Consolidation Fill - Tight", group = grpMarkers)
zoneLooseColor      = input.color(color.new(color.gray, 90), "Consolidation Fill - Not Yet Tight", group = grpMarkers)
zoneConfirmedColor  = input.color(color.new(color.lime, 80), "Confirmed Breakout Zone Fill", group = grpMarkers)
paraWindowColor     = input.color(color.new(color.orange, 85), "Parabolic Window Fill/Line", group = grpMarkers)
linearityWindowColor = input.color(color.new(color.purple, 85), "Linearity Window Fill/Line", group = grpMarkers)

// --- Confirmed zones: unaffected by style toggle, always a permanent box ---
if showConfirmedZones and breakoutConfirmed
    box.new(left=bar_index - boConsolLen, top=consolHigh, right=bar_index, bottom=consolLow,
             border_color=color.new(color.lime, 30), border_width=1, bgcolor=zoneConfirmedColor,
             extend=extend.none, text=str.tostring(lynchCount) + "/6", text_size=size.small,
             text_color=color.white, text_valign=text.align_top, force_overlay=true)

// --- Consolidation window (live) ---
var box consolBox = na
var line consolLineL = na
var line consolLineR = na
if showConsolWindow and barstate.islast
    box.delete(consolBox)
    line.delete(consolLineL)
    line.delete(consolLineR)
    consolFillColor = tightConsolidation ? zoneTightColor : zoneLooseColor
    consolLineColor = tightConsolidation ? color.new(color.aqua, 20) : color.new(color.gray, 40)
    if markerStyle == "Box"
        consolBox := box.new(left=bar_index - boConsolLen, top=consolHigh, right=bar_index - 1, bottom=consolLow,
             border_color=consolLineColor, border_width=1, border_style=line.style_dashed,
             bgcolor=consolFillColor, extend=extend.none,
             text="Consolidation: LIVE " + str.tostring(lynchCount) + "/6", text_size=size.small,
             text_color=color.white, text_valign=text.align_top, force_overlay=true)
    else
        consolLineL := line.new(bar_index - boConsolLen, consolLow, bar_index - boConsolLen, consolHigh,
             color=consolLineColor, width=2, style=line.style_dashed, force_overlay=true)
        consolLineR := line.new(bar_index - 1, consolLow, bar_index - 1, consolHigh,
             color=consolLineColor, width=2, style=line.style_dashed, force_overlay=true)

// --- Parabolic lookback window (live) - what the crash-progress table rows measure ---
var box paraBox = na
var line paraLineL = na
var line paraLineR = na
if showParaWindow and barstate.islast
    box.delete(paraBox)
    line.delete(paraLineL)
    line.delete(paraLineR)
    paraHigh = ta.highest(high, pbLookback)
    paraLow  = ta.lowest(low, pbLookback)
    if markerStyle == "Box"
        paraBox := box.new(left=bar_index - pbLookback, top=paraHigh, right=bar_index, bottom=paraLow,
             border_color=color.new(color.orange, 30), border_width=1, bgcolor=paraWindowColor,
             extend=extend.none, text="Parabolic (" + str.tostring(pbLookback) + "d)", text_size=size.tiny,
             text_color=color.white, text_valign=text.align_bottom, force_overlay=true)
    else
        paraLineL := line.new(bar_index - pbLookback, paraLow, bar_index - pbLookback, paraHigh,
             color=color.orange, width=2, force_overlay=true)
        paraLineR := line.new(bar_index, paraLow, bar_index, paraHigh, color=color.orange, width=2, force_overlay=true)

// --- 2LYNCH Linearity window (live) - what the "L" efficiency-ratio check measures ---
var box linBox = na
var line linLineL = na
var line linLineR = na
if showLinearityWindow and barstate.islast
    box.delete(linBox)
    line.delete(linLineL)
    line.delete(linLineR)
    linHigh = ta.highest(high, boMoveLookback)
    linLow  = ta.lowest(low, boMoveLookback)
    if markerStyle == "Box"
        linBox := box.new(left=bar_index - boMoveLookback, top=linHigh, right=bar_index, bottom=linLow,
             border_color=color.new(color.purple, 30), border_width=1, bgcolor=linearityWindowColor,
             extend=extend.none, text="Linearity (" + str.tostring(boMoveLookback) + "d)", text_size=size.tiny,
             text_color=color.white, text_valign=text.align_top, text_halign=text.align_left, force_overlay=true)
    else
        linLineL := line.new(bar_index - boMoveLookback, linLow, bar_index - boMoveLookback, linHigh,
             color=color.purple, width=2, force_overlay=true)
        linLineR := line.new(bar_index, linLow, bar_index, linHigh, color=color.purple, width=2, force_overlay=true)

// =====================================================================================
// ALERTS
// =====================================================================================
alertcondition(breakoutConfirmed, title="Breakout Setup", message="{{ticker}}: Breakout setup triggered on {{interval}}")
alertcondition(epSetup, title="Episodic Pivot Setup", message="{{ticker}}: EP setup triggered on {{interval}}")
alertcondition(paraShortSetup, title="Parabolic Short Setup", message="{{ticker}}: Parabolic SHORT setup triggered on {{interval}}")
alertcondition(paraLongSetup, title="Parabolic Long Setup", message="{{ticker}}: Parabolic LONG setup triggered on {{interval}}")
alertcondition(breakoutConfirmed or epSetup or paraShortSetup or paraLongSetup, title="Any Qullamaggie Setup", message="{{ticker}}: Qullamaggie setup triggered on {{interval}}")

// =====================================================================================
// DASHBOARD TABLE (quick visual status on the chart you're viewing)
// Reorganized 2026-07-30: previously scattered info about the same setup across
// three separate blocks (all 4 signals up top, then 2LYNCH, then P-Long's
// breakdown, then all 3 scores at the bottom). Now grouped so everything about
// one setup - signal, score, and (where relevant) its own criteria breakdown -
// sits together. Also added Up Move%/Up Streak/Stretch-Above-MA rows for
// Parabolic Short, mirroring the Down Move%/Down Streak/Stretch-Below-MA rows
// Parabolic Long already had - that breakdown existed only for Long before,
// even though the underlying paraMoveUpPct/upStreak/stretchAboveMAPct values
// were already being computed, just never surfaced in the table.
// =====================================================================================
showTable = input.bool(true, "Show Status Table", group = "Display")
tableTextSizeInput = input.string("Small", "Status Table Text Size", options=["Tiny", "Small", "Normal", "Large"], group = "Display",
     tooltip = "Tiny fits the most rows on screen but can be hard to read on some displays. Normal/Large are easier to read but push the table taller.")
tableTextSize = tableTextSizeInput == "Tiny" ? size.tiny : tableTextSizeInput == "Small" ? size.small : tableTextSizeInput == "Large" ? size.large : size.normal
if showTable and barstate.islast
    var table t = table.new(position.top_right, 2, 32, border_width = 1, force_overlay = true)
    hdrBg = color.new(color.gray, 70)
    sectionBg = color.new(color.blue, 85)

    table.cell(t, 0, 0, "Qullamaggie Setups", bgcolor = hdrBg, text_color = color.white, text_size=tableTextSize)
    table.cell(t, 1, 0, "", bgcolor = hdrBg, text_size=tableTextSize)
    table.cell(t, 0, 1, "Liquidity OK", text_size=tableTextSize)
    table.cell(t, 1, 1, liquidityOK ? "YES" : "no", text_color = liquidityOK ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 2, "ADR %", text_size=tableTextSize)
    table.cell(t, 1, 2, str.tostring(adrPct, "#.##"), text_size=tableTextSize)

    // --- BREAKOUT (signal, score, 50MA rule, 2LYNCH - all together) ---
    table.cell(t, 0, 3, "BREAKOUT", bgcolor = sectionBg, text_color = color.white, text_size=tableTextSize)
    table.cell(t, 1, 3, "", bgcolor = sectionBg, text_size=tableTextSize)
    table.cell(t, 0, 4, "Signal", text_size=tableTextSize)
    table.cell(t, 1, 4, breakoutConfirmed ? "SIGNAL" : "-", text_color = breakoutConfirmed ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 5, "Score (0-100)", text_size=tableTextSize)
    table.cell(t, 1, 5, str.tostring(htfScore) + " " + badgeText(htfScore), text_color = badgeColor(htfScore), text_size=tableTextSize)
    table.cell(t, 0, 6, "Above 50MA (Kris's rule)", text_size=tableTextSize)
    table.cell(t, 1, 6, close > sma50 ? "YES" : "no", text_color = close > sma50 ? color.lime : color.red, text_size=tableTextSize)
    table.cell(t, 0, 7, "2LYNCH (Breakout only)", text_size=tableTextSize)
    table.cell(t, 1, 7, str.tostring(lynchCount) + "/6",
               text_color = lynchCount >= 4 ? color.lime : lynchCount >= 2 ? color.yellow : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 8, "  2: not up 2d already", text_size=tableTextSize)
    table.cell(t, 1, 8, lynch2 ? "pass" : "fail", text_color = lynch2 ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 9, "  L: linear move", text_size=tableTextSize)
    table.cell(t, 1, 9, lynchL ? "pass" : "fail", text_color = lynchL ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 10, "  Y: young trend", text_size=tableTextSize)
    table.cell(t, 1, 10, lynchY ? "pass" : "fail", text_color = lynchY ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 11, "  N: narrow/down pre-day", text_size=tableTextSize)
    table.cell(t, 1, 11, lynchN ? "pass" : "fail", text_color = lynchN ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 12, "  C: clean consolidation", text_size=tableTextSize)
    table.cell(t, 1, 12, lynchC ? "pass" : "fail", text_color = lynchC ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 13, "  H: close near high", text_size=tableTextSize)
    table.cell(t, 1, 13, lynchH ? "pass" : "fail", text_color = lynchH ? color.lime : color.gray, text_size=tableTextSize)

    // --- EPISODIC PIVOT (now with its own criteria breakdown, matching what
    // Breakout/2LYNCH and Parabolic Short/Long already had. These 3 rows are the
    // exact 4 gates behind the epSetup boolean above - liquidityOK is already shown
    // at the top of the table, so it's not repeated here) ---
    table.cell(t, 0, 14, "EPISODIC PIVOT", bgcolor = sectionBg, text_color = color.white, text_size=tableTextSize)
    table.cell(t, 1, 14, "", bgcolor = sectionBg, text_size=tableTextSize)
    table.cell(t, 0, 15, "Signal", text_size=tableTextSize)
    table.cell(t, 1, 15, epSetup ? "SIGNAL" : "-", text_color = epSetup ? color.blue : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 16, "Score (0-100)", text_size=tableTextSize)
    table.cell(t, 1, 16, str.tostring(epScore) + " " + badgeText(epScore), text_color = badgeColor(epScore), text_size=tableTextSize)
    table.cell(t, 0, 17, "Gap % (need " + str.tostring(epMinGapPct, "#") + "+)", text_size=tableTextSize)
    table.cell(t, 1, 17, str.tostring(gapPct, "#.#") + "%",
               text_color = gapPct >= epMinGapPct ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 18, "Vol mult (need " + str.tostring(epMinVolMult, "#.#") + "x+)", text_size=tableTextSize)
    table.cell(t, 1, 18, str.tostring(volume / avgVol, "#.##") + "x",
               text_color = volume >= avgVol * epMinVolMult ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 19, "Prior run % (need " + str.tostring(epMaxPriorRunPct, "#") + "-)", text_size=tableTextSize)
    table.cell(t, 1, 19, str.tostring(priorRunPct, "#.#") + "%",
               text_color = notAlreadyExtended ? color.lime : color.gray, text_size=tableTextSize)

    // --- PARABOLIC SHORT (its own criteria breakdown, matching what
    // Parabolic Long already had - see note above) ---
    table.cell(t, 0, 20, "PARABOLIC SHORT", bgcolor = sectionBg, text_color = color.white, text_size=tableTextSize)
    table.cell(t, 1, 20, "", bgcolor = sectionBg, text_size=tableTextSize)
    table.cell(t, 0, 21, "Signal", text_size=tableTextSize)
    table.cell(t, 1, 21, paraShortSetup ? "SIGNAL" : "-", text_color = paraShortSetup ? color.red : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 22, "Score (0-100)", text_size=tableTextSize)
    table.cell(t, 1, 22, str.tostring(pshortScore) + " " + badgeText(pshortScore), text_color = badgeColor(pshortScore), text_size=tableTextSize)
    table.cell(t, 0, 23, "Up move % (need " + str.tostring(pbMinMovePct, "#") + ")", text_size=tableTextSize)
    table.cell(t, 1, 23, str.tostring(paraMoveUpPct, "#.#") + "%",
               text_color = paraMoveUpPct >= pbMinMovePct ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 24, "Up streak (need " + str.tostring(pbMinStreak) + ")", text_size=tableTextSize)
    table.cell(t, 1, 24, str.tostring(upStreak) + "d", text_color = upStreak >= pbMinStreak ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 25, "Stretch above MA (need " + str.tostring(pbMinStretchPct, "#") + ")", text_size=tableTextSize)
    table.cell(t, 1, 25, str.tostring(stretchAboveMAPct, "#.#") + "%",
               text_color = stretchAboveMAPct >= pbMinStretchPct ? color.lime : color.gray, text_size=tableTextSize)

    // --- PARABOLIC LONG (no 0-100 score defined for this one - boolean only,
    // see guide.html; crash-progress breakdown is the prerequisite this signal
    // watches for, not a score) ---
    table.cell(t, 0, 26, "PARABOLIC LONG", bgcolor = sectionBg, text_color = color.white, text_size=tableTextSize)
    table.cell(t, 1, 26, "", bgcolor = sectionBg, text_size=tableTextSize)
    table.cell(t, 0, 27, "Signal", text_size=tableTextSize)
    table.cell(t, 1, 27, paraLongSetup ? "SIGNAL" : "-", text_color = paraLongSetup ? color.orange : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 28, "Crash detected (prerequisite)", text_size=tableTextSize)
    table.cell(t, 1, 28, crashDetected ? "CRASH" : "-", text_color = crashDetected ? color.orange : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 29, "Down move % (need " + str.tostring(pbMinMovePct, "#") + ")", text_size=tableTextSize)
    table.cell(t, 1, 29, str.tostring(paraMoveDownPct, "#.#") + "%",
               text_color = paraMoveDownPct >= pbMinMovePct ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 30, "Down streak (need " + str.tostring(pbMinStreak) + ")", text_size=tableTextSize)
    table.cell(t, 1, 30, str.tostring(downStreak) + "d",
               text_color = downStreak >= pbMinStreak ? color.lime : color.gray, text_size=tableTextSize)
    table.cell(t, 0, 31, "Stretch below MA (need " + str.tostring(pbMinStretchPct, "#") + ")", text_size=tableTextSize)
    table.cell(t, 1, 31, str.tostring(stretchBelowMAPct, "#.#") + "%",
               text_color = stretchBelowMAPct >= pbMinStretchPct ? color.lime : color.gray, text_size=tableTextSize)

Overview

Qullamaggie Setups - Breakout / EP / Parabolic evaluates prior moves, consolidation tightness, volume expansion, moving average alignment (50 SMA and 20 EMA), and stretch from moving averages to generate setup scores from 0 to 100. It draws visual consolidation boxes, setup markers, and an info table on the main chart while displaying a volume histogram in a separate pane. Key inputs allow filtering by ADR percentage, dollar volume, consolidation rules, and gap thresholds. Signals on live bars may update until the bar closes.

Details

Published by

axidzz on GitHub

Code credit

YourNameHere, as written in the script's header

Type

Indicator, Pine Script v5

Draws

Separate pane below the chart

Category

Oscillators & momentum, Breakout, Screeners & dashboards

Markets

Stocks

Timeframes

H4 and above

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

momentum-strategypine-scriptscannertrade-setupstradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
ADR% Lengthint20adrLen
Min ADR % (avg daily range)float3.0minADR
Volume Avg Lengthint20volAvgLen
Min Avg Dollar Volume ($M)float5.0minDollarVolM
Min Pricefloat2.0minPrice
Prior-Move Lookback (bars)int60boMoveLookback
Min Prior Move %float30boMinMovePct
Consolidation Length (bars, excludes today)int15boConsolLen
Max Consolidation Range %float15boMaxRangePct
Breakout Volume >= x * Avg Volumefloat1.5boVolExpMult
Require Close > EMA20 on breakout barbooltrueboRequireAboveEMA
Require Close > 50-day SMA (Kris's stated hard rule: 'never buys below 50MA')booltrueboRequireAbove50MA
Show all 40 settings
SettingTypeDefaultVariable
Min Gap Up %float10epMinGapPct
Volume >= x * Avg Volumefloat3.0epMinVolMult
'No Prior Run' Lookback (bars, ~6mo)int120epNoRunLookback
Max Prior Run % (skip if already extended)float20epMaxPriorRunPct
Parabolic Move Lookback (bars)int10pbLookback
Min Parabolic Move % (up for short, down for long)float50pbMinMovePct
Min Consecutive Up/Down Closesint4pbMinStreak
Stretch Reference MA Lengthint20pbMAlen
Min % Stretch From MAfloat30pbMinStretchPct
Bars After Crash To Look For Long Reversalint5pbLongLookbackBars
L: Min Efficiency Ratio (linearity)float0.35lynchEfficiencyMin
Y: Max Bars Since Below 50-day SMA (young trend)int90lynchYoungMaxBars
C: Sharp Down Day Threshold %float4.0lynchDownDayPct
C: Max Sharp Down Days Allowed in Baseint1lynchMaxDownDays
H: Min Close Position in Bar's Range (0-1)float0.7lynchCloseRangeMin
Require Breakout to also pass Strict 2LYNCHboolfalserequireStrict
Strict: Min 2LYNCH Count Required (of 6)int4strictMinCount
Marker Style (applies to all live windows below)stringBoxmarkerStyle
Show Confirmed Breakout Zones (Consolidation, historical)booltrueshowConfirmedZones
Show Consolidation Window (live)boolfalseshowConsolWindow
Show Parabolic Lookback Window (live)boolfalseshowParaWindow
Show 2LYNCH Linearity Window (live)boolfalseshowLinearityWindow
Consolidation Fill - Tightcolorcolor.new(color.aqua, 85)zoneTightColor
Consolidation Fill - Not Yet Tightcolorcolor.new(color.gray, 90)zoneLooseColor
Confirmed Breakout Zone Fillcolorcolor.new(color.lime, 80)zoneConfirmedColor
Parabolic Window Fill/Linecolorcolor.new(color.orange, 85)paraWindowColor
Linearity Window Fill/Linecolorcolor.new(color.purple, 85)linearityWindowColor
Show Status TablebooltrueshowTable
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

Pattern Recognition [WavesUnchained] automatically identifies chart patterns including ascending, descending, and symmetrical triangles, rising and falling wedges, and parallel channels. It validates formations through multi-point pivot analysis, volume spike

Indicator v6 BreakoutSupport & resistance Repaints
Updated yesterday · MIT GitHub repository of Pattern Recognition [WavesUnchained]

Schaff Trend Cycle

by everget
948 stars

This indicator plots the Schaff Trend Cycle in a separate pane. It subtracts a slow exponential moving average from a fast one, applies a stochastic cycle twice, and smooths each result to form a 0–100 oscillator. Line color follows the slope, or the upper and

Indicator v4 Oscillators & momentumTrend
Updated 10 days ago · GPL-3.0 GitHub repository of Schaff Trend Cycle

This indicator scans a pasted list of symbols for RSI directional momentum and shows matching long and short signals in a table in a separate pane. The user chooses continuous break, continuous retracement, flip break, flip retracement, divergence, or hidden d

Indicator v6 Oscillators & momentumDivergence
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup - RSI Directional Momentum Scanner
261 stars

This indicator plots the RSI oscillator on the main chart and displays its value distribution in a table positioned at the top right. The script calculates the relative strength index using the input source and length, then plots horizontal lines at 0, 50, and

Indicator v4 Oscillators & momentumScreeners & dashboards
Updated 4 years ago · None stated GitHub repository of Tables-003 Oscillator Distribution

Market Speedometer

by TradersPost
218 stars

Market Speedometer plots a 0-100 market speed reading in a separate pane. It measures movement by bar-to-bar change, true range, or a short average of recent bars, then ranks that movement against ATR and a percentile lookback. An EMA smooths the reading, and

Indicator v6 Volatility & bandsOscillators & momentum
Updated 3 months ago · None stated GitHub repository of Market Speedometer

KamaFama

by Mastaaa1987
141 stars

KamaFama is an indicator that translates a Freqtrade trading strategy into Pine Script for visualization. It combines Kaufman Adaptive Moving Average (KAMA), MAMA/FAMA calculations derived via Hilbert Transform, Williams %R, and correlation trend filters to ev

Indicator v6 Moving averagesOscillators & momentum
Updated 6 weeks ago · MIT GitHub repository of KamaFama

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers