Qullamaggie_Setups.pine
Copy
// 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)
Show all 551 lines