Read the market.
Direction, velocity, acceleration, curvature, path efficiency, volatility and participation retain Anatomy’s explicit definitions.
Konatus Bearing · Pine Script v6 · free
One bearing line, colored candles and a dashboard that says, in plain words, what the market is doing and what the plan is waiting for. It reads stocks, ETFs, futures, bonds, forex and crypto, and it writes the stop before you enter.
Get the Pine script New: Market Anatomy code ↓ Read the quick guide ↓
Experimental. Version 0.3.1 has not yet been compiled in TradingView, and no market edge has been measured. Plans on the chart are illustrations, not orders.

KONATUS BEARINGStocks
WAIT SETUP
Evidence ready; wait for setup
kb-0.3 · experimental · illustrative
Entry markers have no text. Triangles mark confirmed entry intentions; the illustrative fill is at the next open. Shorts must be enabled and pass the same setup and risk gates.
Only closed candles are colored, and the dashboard reads the last closed bar. A color is never advice to enter.
The large line on the panel is the plan. The small line under it says why.
With no plan open, the big word is the New entry status: the first gate that is still closed.
Breakout waits for two closes beyond the prior 20-bar range. Pullback waits for a close that crosses back over the bearing in the direction of the trend. Either takes whichever comes first. Every other gate still applies. At entry the stop is set 3 ATR away and frozen; size comes from account, risk % and that distance, capped by your capacity limit, and never from how confident the reading looks.
Switch the panel to Detailed for drift, model uncertainty, run length and efficiency. Theme follows your chart on Auto.
Konatus Bearing 0.3.1, complete Pine v6 source, free to copy, use and share. Download or copy the entire file, paste it into a blank Pine Editor indicator, save and add it to your chart.
Download Konatus_Bearing_0.3.1.txt
//@version=6
indicator("Konatus Bearing · Cross-market 0.3.1", "Konatus031", overlay = true, max_bars_back = 2000)
// Copyright (c) 2026 Kontaus. Experimental kb-0.3; no established market edge.
bool allowShort = input.bool(false, "Enable short entries", group = "00 · Entry direction", tooltip = "Long entries are enabled. Turn this on to also allow red short-entry triangles and short plans. Signals still require all setup and risk conditions. This control is in the cross-market 0.3.1 indicator; the old 0.2 baseline is long-only.")
string profileInput = input.string("Auto", "Instrument profile", options = ["Auto", "Stocks", "Bonds", "Futures", "Forex", "Crypto", "Generic"], group = "01 · Market")
string profile = profileInput == "Auto" ? (syminfo.type == "stock" or syminfo.type == "fund" ? "Stocks" : syminfo.type == "futures" ? "Futures" : syminfo.type == "forex" ? "Forex" : syminfo.type == "crypto" ? "Crypto" : "Generic") : profileInput
string volumePolicy = input.string("Profile", "Participation policy", options = ["Profile", "Required", "Price only"], group = "01 · Market", tooltip = "Profile requires volume for Stocks/Futures/Crypto. Bonds/Forex/Generic use price only. Missing required volume blocks new plans.")
string volumeMode = input.string("Prior EW", "Volume baseline", options = ["Prior EW", "Session slot"], group = "01 · Market", tooltip = "Prior EW uses 20 earlier volume observations. Session slot uses at least 5 earlier trading days at this exchange-local minute. Minute or hourly charts only; no current-bar volume in the baseline.")
bool vwapGate = input.bool(false, "Require session VWAP alignment", group = "01 · Market", tooltip = "Requires a fully observed trading-day anchor and positive current volume. VWAP is context, not order flow.")
int resetAt = input.time(0, "Reset model at contract/data boundary", group = "01 · Market", tooltip = "Optional timestamp. Resets analytical history once; never cancels an open illustrative stop. Does not construct or adjust a futures roll.")
string setupMode = input.string("Breakout", "Setup", options = ["Breakout", "Pullback", "Either"], group = "02 · Signals")
float decay = input.float(0.9, "Evidence memory", minval = 0.01, maxval = 0.99, step = 0.01, group = "02 · Signals")
float persistenceMin = input.float(20, "Minimum directional evidence", minval = 0, maxval = 100, group = "02 · Signals")
float extensionLimit = input.float(1.5, "Maximum entry extension · ATR", minval = 0.1, group = "02 · Signals")
float efficiencyMin = input.float(0.25, "Minimum directional efficiency", minval = 0, maxval = 1, group = "02 · Signals")
float shockAtr = input.float(4, "Shock threshold · prior ATR", minval = 0.1, group = "02 · Signals")
int cooldown = input.int(3, "Shock cooldown · bars", minval = 0, maxval = 100, group = "02 · Signals")
float account = input.float(100000, "Reference account", minval = 1, group = "03 · Risk")
float riskPct = input.float(1, "Planned risk %", minval = 0, maxval = 100, group = "03 · Risk")
float stopAtr = input.float(3, "Frozen distance · ATR", minval = 0.1, group = "03 · Risk")
float conversion = input.float(1, "Account currency per quote currency", minval = 0.00000001, group = "03 · Risk", tooltip = "Explicit fixed conversion for the illustration, not an FX feed. Use 1 only when account and quote currency match. Recompute when conversion changes.")
float pointOverride = input.float(0, "Point-value override · 0 = symbol", minval = 0, group = "03 · Risk")
float pointValue = pointOverride > 0 ? pointOverride : syminfo.pointvalue
float tick = syminfo.mintick
float lot = input.float(1, "Quantity increment", minval = 0.00000001, group = "03 · Risk", tooltip = "Quantity uses symbol units/contracts, not broker-defined FX lots. Confirm the point value and increment for this instrument.")
float margin = input.float(0, "Capacity per unit · account currency", minval = 0, group = "03 · Risk", tooltip = "Required for Futures and nonpositive-price plans. Enter a conservative per-contract allocation. It is not a live broker margin model. Zero uses cash notional for other positive-price instruments.")
float capitalFraction = input.float(1, "Maximum capacity · account fraction", minval = 0, maxval = 1, group = "03 · Risk")
float costBps = input.float(5, "One-way notional costs · bps", minval = 0, group = "03 · Risk")
float feePerUnit = input.float(0, "One-way fee per unit · quote currency", minval = 0, group = "03 · Risk")
int slipTicks = input.int(1, "Adverse slippage · ticks", minval = 0, maxval = 100, group = "03 · Risk")
string theme = input.string("Auto", "Theme", options = ["Auto", "Dark", "Light"], group = "04 · Appearance")
string density = input.string("Essential", "Dashboard", options = ["Essential", "Detailed", "Hidden"], group = "04 · Appearance")
bool candleColors = input.bool(true, "Color confirmed candles", group = "04 · Appearance")
bool showVwap = input.bool(false, "Show session VWAP", group = "04 · Appearance")
if not chart.is_standard or timeframe.isticks
runtime.error("Konatus requires standard time-based OHLC charts.")
if na(pointValue) or na(tick) or math.sign(pointValue) <= 0 or math.sign(tick) <= 0
runtime.error("A positive tick and point value are required.")
if volumeMode == "Session slot" and (not timeframe.isintraday or timeframe.in_seconds() < 60)
runtime.error("Session-slot volume needs intraday time bars of at least one minute.")
bool needsVolume = volumePolicy == "Required" or (volumePolicy == "Profile" and (profile == "Stocks" or profile == "Futures" or profile == "Crypto"))
if vwapGate and not timeframe.isintraday
runtime.error("Session VWAP gating requires intraday time bars. Disable it on daily and higher charts.")
// Shared confirmed-bar analytical engine. Price units allow zero/negative prices.
var float level = na
var float drift = 0
var float p00 = 0
var float p01 = 0
var float p11 = 0
var float atr = 0
var float trSum = 0
var float previous = na
var float evidence = 0
var int count = 0
var int direction = 0
var int runLength = 0
var int pause = 0
var array<float> highs = array.new_float()
var array<float> lows = array.new_float()
var array<float> closes = array.new_float()
var array<float> slotMean = array.new_float(1440, 0)
var array<int> slotCount = array.new_int(1440, 0)
var array<int> slotAnchor = array.new_int(1440, na)
var float volMean = 0
var int volCount = 0
var int anchor = na
var bool anchorComplete = false
var bool anchorValid = true
var float pv = 0
var float vSum = 0
var float vwap = na
var float relativeVolume = na
var float efficiency = 0
var float extension = na
var float surprise = na
var float predictiveSd = na
var float distance = 0
var float quantity = 0
var string volumeStatus = "WARMUP"
var string condition = "WARMUP"
var string entryStatus = "WARMUP"
var string setupName = "NONE"
var int breakoutSide = 0
var float breakoutLevel = na
var bool ready = false
int signal = 0
bool valid = not na(open) and not na(high) and not na(low) and not na(close) and (math.min(open, close) - low) / tick >= 0 and (high - math.max(open, close)) / tick >= 0
bool resetNow = resetAt > 0 and time >= resetAt and (na(time[1]) or time[1] < resetAt)
if barstate.isconfirmed
if not valid or resetNow
level := na
drift := 0
p00 := 0
p01 := 0
p11 := 0
atr := 0
trSum := 0
previous := na
evidence := 0
count := 0
direction := 0
runLength := 0
pause := 0
array.clear(highs)
array.clear(lows)
array.clear(closes)
array.fill(slotMean, 0)
array.fill(slotCount, 0)
array.fill(slotAnchor, na)
volMean := 0
volCount := 0
anchor := na
anchorComplete := false
anchorValid := true
pv := 0
vSum := 0
vwap := na
breakoutSide := 0
ready := false
quantity := 0
distance := 0
extension := na
efficiency := 0
surprise := na
predictiveSd := na
relativeVolume := na
volumeStatus := "UNAVAILABLE"
condition := "INVALID DATA"
entryStatus := "INVALID DATA"
setupName := "NONE"
if valid
float priorLevel = level
float tr = na(previous) ? high - low : math.max(high - low, math.abs(high - previous), math.abs(low - previous))
float scale = math.max(count >= 14 ? atr : count > 0 ? trSum / count : tr, tick)
float variance = scale * scale
float observation = 0.25 * variance
if na(level)
level := close
p00 := observation
p11 := observation
surprise := 0
predictiveSd := math.sqrt(2 * observation)
else
float a = p00 + 2 * p01 + p11 + 0.01 * variance
float b = p01 + p11
float d = p11 + 0.0001 * variance
float predictive = a + observation
float innovation = close - level - drift
surprise := innovation / math.sqrt(predictive)
predictiveSd := math.sqrt(predictive)
float k0 = a / predictive
float k1 = b / predictive
float m = 1 - k0
level += drift + k0 * innovation
drift += k1 * innovation
p00 := m * m * a + k0 * k0 * observation
p01 := m * (b - k1 * a) + k0 * k1 * observation
p11 := d - 2 * k1 * b + k1 * k1 * a + k1 * k1 * observation
count += 1
if count <= 14
trSum += tr
atr := trSum / count
else
atr += (tr - atr) / 14
int priorDirection = direction
direction := drift / tick > 0.000001 ? 1 : drift / tick < -0.000001 ? -1 : 0
runLength := direction != 0 and direction == priorDirection ? runLength + 1 : direction != 0 ? 1 : 0
evidence := decay * evidence + (1 - decay) * math.max(-1, math.min(1, drift / (0.1 * scale)))
array.push(closes, close)
if array.size(closes) > 21
array.shift(closes)
float travel = 0
if array.size(closes) > 1
for j = 1 to array.size(closes) - 1
travel += math.abs(array.get(closes, j) - array.get(closes, j - 1))
efficiency := travel > 0 ? math.abs(close - array.get(closes, 0)) / travel : 0
extension := math.abs(close - level) / math.max(atr, tick)
bool shock = not na(previous) and math.abs(close - previous) / scale > shockAtr
pause := shock ? cooldown : math.max(0, pause - 1)
ready := count >= 40 and atr / tick > 0.000001
condition := count < 40 ? "WARMUP" : not ready ? "FLAT" : shock or pause > 0 ? "SHOCK" : efficiency < efficiencyMin ? "CONGESTION" : "TREND"
bool haveVolume = not na(volume) and volume >= 0
int slot = hour(time, syminfo.timezone) * 60 + minute(time, syminfo.timezone)
int dayAnchor = time_tradingday
float baseline = volumeMode == "Session slot" ? array.get(slotMean, slot) : volMean
int samples = volumeMode == "Session slot" ? array.get(slotCount, slot) : volCount
int lastAnchor = array.get(slotAnchor, slot)
bool duplicateSlot = volumeMode == "Session slot" and not na(lastAnchor) and lastAnchor == dayAnchor
bool enough = samples >= (volumeMode == "Session slot" ? 5 : 20)
relativeVolume := haveVolume and enough and baseline > 0 and not duplicateSlot ? volume / baseline : na
volumeStatus := not haveVolume ? "VOLUME MISSING" : duplicateSlot ? "SLOT REPEATED" : volume == 0 ? "ZERO VOLUME" : na(relativeVolume) ? "VOLUME WARMUP" : "AVAILABLE"
if haveVolume and not duplicateSlot
float updated = samples == 0 ? volume : baseline + (volume - baseline) * 2 / 21
if volumeMode == "Session slot"
array.set(slotMean, slot, updated)
array.set(slotCount, slot, samples + 1)
array.set(slotAnchor, slot, dayAnchor)
else
volMean := updated
volCount += 1
if not needsVolume
volumeStatus := "PRICE ONLY"
bool volumeOk = not needsVolume or (volumeStatus == "AVAILABLE" and relativeVolume >= 1)
if na(anchor) or dayAnchor != anchor
anchorComplete := not na(anchor)
anchor := dayAnchor
anchorValid := true
pv := 0
vSum := 0
if not haveVolume
anchorValid := false
if haveVolume and volume > 0
pv += hlc3 * volume
vSum += volume
vwap := vSum > 0 ? pv / vSum : na
bool contextOk = not vwapGate or (anchorValid and anchorComplete and not na(vwap) and haveVolume and volume > 0 and direction * (close - vwap) / tick > 0)
int fire = 0
if not ready or shock or pause > 0
breakoutSide := 0
else if breakoutSide != 0
if breakoutSide * (close - breakoutLevel) / tick > 0
fire := breakoutSide
breakoutSide := 0
else if array.size(highs) == 20
if (close - array.max(highs)) / tick > 0
breakoutSide := 1
breakoutLevel := array.max(highs)
else if (array.min(lows) - close) / tick > 0
breakoutSide := -1
breakoutLevel := array.min(lows)
int pull = not na(previous) and not na(priorLevel) and direction * (previous - priorLevel) / tick <= 0 and direction * (close - level) / tick > 0 ? direction : 0
int setupSide = setupMode == "Breakout" ? fire : setupMode == "Pullback" ? pull : fire != 0 ? fire : pull
setupName := setupSide != 0 and setupSide == fire ? "BREAKOUT" : setupSide != 0 ? "PULLBACK" : "NONE"
distance := math.ceil(stopAtr * atr / tick - 1e-9) * tick
float slip = slipTicks * tick
float fees = 2 * feePerUnit + (2 * math.abs(close) + distance + 2 * slip) * pointValue * costBps / 10000
float unitRisk = ((distance + slip + tick) * pointValue + fees) * conversion
float capacity = profile == "Futures" or math.sign(close) <= 0 ? margin : margin > 0 ? margin : (math.abs(close) + slip) * pointValue * conversion
float capacityWithFees = capacity + (feePerUnit + (math.abs(close) + slip) * pointValue * costBps / 10000) * conversion
quantity := math.sign(capacity) > 0 and math.sign(distance) > 0 and math.sign(unitRisk) > 0 ? math.max(0, math.floor(math.min(account * riskPct / 100 / unitRisk, account * capitalFraction / capacityWithFees) / lot) * lot) : 0
entryStatus := count < 40 ? "WARMUP" : not ready ? "FLAT" : condition == "SHOCK" ? "SHOCK PAUSE" : condition == "CONGESTION" ? "CONGESTION" : direction == 0 or direction * evidence * 100 < persistenceMin ? "WEAK EVIDENCE" : direction < 0 and not allowShort ? "SHORT DISABLED" : not volumeOk and volumeStatus != "AVAILABLE" ? volumeStatus : not volumeOk ? "LOW PARTICIPATION" : not contextOk ? "VWAP CONTEXT" : extension > extensionLimit ? "EXTENDED" : quantity <= 0 ? "SIZE UNAVAILABLE" : setupSide == direction ? "READY" : "WAIT SETUP"
signal := entryStatus == "READY" ? direction : 0
array.push(highs, high)
array.push(lows, low)
if array.size(highs) > 20
array.shift(highs)
array.shift(lows)
previous := close
// Illustrative next-open ledger. Orders are not sent to a broker.
var int heldSide = 0
var int pendingSide = 0
var float pendingQuantity = 0
var float pendingDistance = 0
var float heldQuantity = 0
var float entryPrice = na
var float stopPrice = na
var float heldDistance = na
var int reversalCount = 0
var bool pendingExit = false
var bool capacityBreached = false
bool entryIntent = false
bool exitIntent = false
bool entered = false
bool exited = false
string exitReason = ""
if barstate.isconfirmed and valid
bool dueExit = pendingExit
pendingExit := false
float slip = slipTicks * tick
if heldSide != 0
if heldSide * (open - stopPrice) / tick <= 0
exited := true
exitReason := "GAP_STOP"
else if dueExit
exited := true
exitReason := "REVERSAL"
if heldSide == 0 and pendingSide != 0
heldSide := pendingSide
heldQuantity := pendingQuantity
heldDistance := pendingDistance
entryPrice := open + heldSide * slip
float rawStop = entryPrice - heldSide * heldDistance
stopPrice := (heldSide > 0 ? math.floor(rawStop / tick + 1e-9) : math.ceil(rawStop / tick - 1e-9)) * tick
float entryFee = heldQuantity * (feePerUnit + math.abs(entryPrice) * pointValue * costBps / 10000) * conversion
float filledCapacity = margin > 0 ? margin : math.abs(entryPrice) * pointValue * conversion
capacityBreached := filledCapacity * heldQuantity + entryFee > account * capitalFraction
pendingSide := 0
entered := true
if heldSide != 0 and not exited and (heldSide > 0 ? (stopPrice - low) / tick >= 0 : (high - stopPrice) / tick >= 0)
exited := true
exitReason := "STOP"
if exited
heldSide := 0
reversalCount := 0
if heldSide != 0
reversalCount := direction == -heldSide ? reversalCount + 1 : 0
if reversalCount >= 2
pendingExit := true
exitIntent := true
else if not exited and signal != 0
pendingSide := signal
pendingQuantity := quantity
pendingDistance := distance
entryIntent := true
// Alerts are decision events. Namespace separates configurations/alert instances.
string alertNamespace = input.string("default", "Alert instance ID", group = "05 · Alerts", tooltip = "Use a distinct ID for each configuration. Allowed characters: letters, numbers, dash and underscore. Recreate TradingView alerts after settings change.")
if str.length(alertNamespace) == 0 or str.match(alertNamespace, "^[A-Za-z0-9_-]+$") != alertNamespace
runtime.error("Alert instance ID must contain only letters, numbers, dash or underscore.")
if entryIntent or exitIntent
string eventName = entryIntent ? "ENTRY_INTENT" : "EXIT_INTENT"
int eventSide = entryIntent ? pendingSide : heldSide
string eventId = "kb-0.3:" + alertNamespace + ":" + syminfo.tickerid + ":" + timeframe.period + ":" + str.tostring(time_close) + ":" + eventName + ":" + str.tostring(eventSide)
string payload = '{"version":"kb-0.3-experimental","id":"' + eventId + '","event":"' + eventName + '","symbol":"' + syminfo.tickerid + '","timeframe":"' + timeframe.period + '","time_close":' + str.tostring(time_close) + ',"side":' + str.tostring(eventSide) + ',"quantity":' + str.tostring(entryIntent ? pendingQuantity : heldQuantity, "#.########") + ',"distance":' + str.tostring(entryIntent ? pendingDistance : heldDistance, "#.########") + ',"illustrative":true}'
alert(payload, alert.freq_once_per_bar_close)
bool dark = theme == "Dark" or (theme == "Auto" and color.r(chart.bg_color) + color.g(chart.bg_color) + color.b(chart.bg_color) < 384)
color surface = dark ? #151B18 : #FCFCF8
color ink = dark ? #E8ECE6 : #202E25
color secondary = dark ? #A5B0A7 : #546559
color up = dark ? #A6CF9A : #37672C
color down = dark ? #EDABA0 : #9E453A
color amber = dark ? #E7C184 : #835B1C
color edge = dark ? #344238 : #BDC7BB
color bearingInk = not ready or math.abs(evidence * 100) < persistenceMin ? secondary : direction > 0 ? up : down
plot(level, "Bearing price", color = bearingInk, linewidth = 2)
plot(showVwap ? vwap : na, "Session VWAP", color = secondary)
plot(heldSide != 0 ? entryPrice : na, "Illustrative entry", color = secondary, style = plot.style_linebr)
plot(heldSide != 0 ? stopPrice : na, "Frozen protective stop", color = amber, linewidth = 2, style = plot.style_linebr)
barcolor(candleColors and barstate.isconfirmed ? bearingInk : na)
plotshape(entryIntent and pendingSide == 1, "Long intent", style = shape.triangleup, location = location.belowbar, color = color.green, size = size.tiny)
plotshape(entryIntent and pendingSide == -1, "Short intent", style = shape.triangledown, location = location.abovebar, color = color.red, size = size.tiny)
plotshape(exited, "Illustrative exit", style = shape.xcross, location = location.abovebar, color = amber, size = size.tiny)
bool displayValid = barstate.isconfirmed ? valid : valid[1]
string state = not displayValid ? "DATA UNAVAILABLE" : pendingExit ? "EXIT QUEUED" : heldSide != 0 ? (heldSide > 0 ? "LONG PLAN" : "SHORT PLAN") : pendingSide != 0 ? "ENTRY QUEUED" : entryStatus
string reason = heldSide != 0 ? (not displayValid ? "Data invalid; last stop retained" : capacityBreached ? "Capacity exceeded at fill" : "Frozen stop active") : pendingSide != 0 ? "Illustrative next-open fill" : entryStatus == "EXTENDED" ? "Trend persists; wait for entry" : entryStatus == "SIZE UNAVAILABLE" ? "Check contract / margin / risk" : entryStatus == "SHOCK PAUSE" ? "New entries paused" : entryStatus == "WAIT SETUP" ? "Evidence ready; wait for setup" : "Experimental · closed-bar state"
var table panel = table.new(position.top_right, 2, 15, bgcolor = surface, frame_color = edge, frame_width = 1)
row(table t, int n, string label, string value, color valueInk, color bg, color labelInk) =>
table.cell(t, 0, n, " " + label + " ", bgcolor = bg, text_color = labelInk, text_halign = text.align_left, text_size = size.small)
table.cell(t, 1, n, " " + value + " ", bgcolor = bg, text_color = valueInk, text_halign = text.align_right, text_size = size.small)
if barstate.isfirst
table.merge_cells(panel, 0, 1, 1, 1)
table.merge_cells(panel, 0, 2, 1, 2)
table.merge_cells(panel, 0, 14, 1, 14)
if barstate.islast
table.clear(panel, 0, 0, 1, 14)
if density != "Hidden" or (heldSide != 0 and (capacityBreached or not displayValid))
row(panel, 0, "KONATUS BEARING", profile, ink, surface, secondary)
table.cell(panel, 0, 1, " " + state + " ", text_color = heldSide != 0 and not pendingExit ? bearingInk : amber, bgcolor = surface, text_size = size.large, text_halign = text.align_left)
table.cell(panel, 0, 2, " " + reason + " ", text_color = secondary, bgcolor = surface, text_size = size.small, text_halign = text.align_left)
row(panel, 3, "DIRECTION", direction > 0 ? "Upward" : direction < 0 ? "Downward" : "Unclear", bearingInk, surface, secondary)
row(panel, 4, "PERSISTENCE", str.tostring(evidence * 100, "0.0") + " / 100", ink, surface, secondary)
row(panel, 5, "CONDITIONS", condition, ink, surface, secondary)
row(panel, 6, "NEW ENTRY", entryStatus, ink, surface, secondary)
row(panel, 7, "EXTENSION", na(extension) ? "—" : str.tostring(extension, "0.00") + " ATR", ink, surface, secondary)
row(panel, 8, "PARTICIPATION", volumeStatus, secondary, surface, secondary)
row(panel, 9, "FROZEN STOP", heldSide != 0 ? str.tostring(stopPrice, format.mintick) : "—", amber, surface, secondary)
row(panel, 10, "ACTIVE / QUEUED QTY", str.tostring(heldSide != 0 ? heldQuantity : pendingSide != 0 ? pendingQuantity : 0, "#.########"), ink, surface, secondary)
if density == "Detailed"
row(panel, 11, "DRIFT / MODEL SD", str.tostring(drift, "#.########") + " / " + str.tostring(math.sqrt(math.max(p11, 0)), "#.########"), ink, surface, secondary)
row(panel, 12, "RUN / EFFICIENCY", str.tostring(runLength) + " / " + str.tostring(efficiency, "0.00"), ink, surface, secondary)
row(panel, 13, "QUOTE → ACCOUNT", syminfo.currency + " × " + str.tostring(conversion, "#.########"), secondary, surface, secondary)
table.cell(panel, 0, 14, " kb-0.3 · experimental · illustrative ", text_color = secondary, bgcolor = surface, text_size = size.small)
plot(evidence * 100, "Persistence", display = display.data_window)
plot(direction, "Direction", display = display.data_window)
plot(extension, "Extension ATR", display = display.data_window)
plot(efficiency, "Efficiency", display = display.data_window)
plot(atr, "ATR", display = display.data_window)
plot(relativeVolume, "Prior relative volume", display = display.data_window)
plot(surprise, "Standardized forecast innovation", display = display.data_window)
plot(predictiveSd, "Prior predictive SD", display = display.data_window)
plot(drift, "Price drift", display = display.data_window)
plot(math.sqrt(math.max(p11, 0)), "Model drift SD", display = display.data_window)
plot(signal, "Setup signal", display = display.data_window)
plot(entryIntent ? pendingSide : 0, "Entry intent", display = display.data_window)
plot(exitIntent ? 1 : 0, "Exit intent", display = display.data_window)
plot(quantity, "Planned quantity", display = display.data_window)
plot(distance, "Planned distance", display = display.data_window)
plot(ready ? 1 : 0, "Ready", display = display.data_window)
// Raw observation exports allow an actual chart-to-Python parity check.
plot(bar_index, "Observation index", display = display.data_window)
plot(open, "Input open", display = display.data_window)
plot(high, "Input high", display = display.data_window)
plot(low, "Input low", display = display.data_window)
plot(close, "Input close", display = display.data_window)
plot(volume, "Input volume", display = display.data_window)
plot(time_tradingday, "Input anchor", display = display.data_window)
plot(hour(time, syminfo.timezone) * 60 + minute(time, syminfo.timezone), "Input slot", display = display.data_window)
plot(anchorComplete ? 1 : 0, "Input anchor complete", display = display.data_window)
plot(resetNow ? 1 : 0, "Input reset", display = display.data_window)
plot(heldSide, "Held side", display = display.data_window)
plot(heldSide != 0 ? heldQuantity : 0, "Held quantity", display = display.data_window)
plot(exited ? 1 : 0, "Illustrative exit flag", display = display.data_window)
plot(exitReason == "STOP" ? 1 : exitReason == "GAP_STOP" ? 2 : exitReason == "REVERSAL" ? 3 : 0, "Illustrative exit code", display = display.data_window)
Version 0.3.1 moves Enable short entries to the first input group, 00 · Entry direction. Long entries use green triangles, shorts use red triangles, and exits keep their X marks. Entry text is removed. The underlying cross-market signal rules and sizing are unchanged. 68 tests pass on the Python reference implementation; TradingView compilation is still pending.
I built this overlay because the usual chart was teaching me the wrong lesson. A moving average, another moving average, MACD, RSI, a stochastic, a channel: the same prices recoded until they looked like a committee. Spinoza’s name for that error is inadequate ideas stacked until they feel like knowledge. When the stack disagreed I invented a reason. When it agreed I felt confirmed. Neither feeling was a plan.
What I needed was one readable heading, one named refusal, and one invalidation written at the moment of the event. Konatus Bearing is that instrument. It names the conditions, names what is missing, and writes the stop before hope can rewrite it. That is discipline, not a forecast.
People call a rule a loser when it is wrong most of the time. That is a hit-rate complaint. Money does not settle hit rate. Chapter 3 of The Spinozan Trader writes expectancy as pW − (1 − p)L − C: the chance of a win times the average win, minus the chance of a loss times the average loss, minus costs. A 90% rule that wins a little and loses a lot can drain an account. A 30% rule that wins enough and loses little can grow one.
| Look at | What it answers | What it does not answer |
|---|---|---|
| Win rate | How often the row is green. | How much money moved. |
| Average win | Conditional size of a gain. | Whether the next trade wins. |
| Average loss | Conditional size of a loss. | Whether the next trade loses. |
| Expectancy | pW − (1−p)L − C. The question that replaces “does it win often?” | Whether buy-and-hold would have done better. |
Full statement: proposition 4 in chapter 3. Why a useful indicator can still fail to produce a lasting edge: chapter 7.
The Conatus Scanner ranks every US-listed company by its causes: power, persistence, self-funding, striving and independence. It tells you which businesses are worth watching. Konatus Bearing reads the chart and tells you when the conditions are right for a plan.
02 / New, separate indicator
A standalone KONATUS indicator with its own Pine script. Install it separately from Konatus Bearing.
Pine Script v6 · Research release 0.1.0

A common language for reading a market: its direction, pace, organization, volatility, location and participation. Built from The Spinozan Trader, this overlay explains the evidence behind its reading and separates observations from hypotheses about their economic causes.
Finite differences measure velocity and acceleration. Normalized curvature describes local bending; Euclidean path geometry compares distance traveled with progress made. A prior mean true range puts price movement on a common scale across instruments. These are measurements of the observed path, not a forecast.
Research release 0.1.0. 21 offline Python reference tests pass. Native TradingView compilation, chart behavior and alert delivery remain unverified. No trading edge has been measured.
Download Pine v6 sourceDownload research package (.zip)
//@version=6
// Konatus Market Anatomy 0.1.0 — experimental interpretation tool.
// Copyright (c) 2026 Kutty Stauder. No profitability or causal identification claim.
// Formula/verification contract: SPEC.md and docs/VERIFICATION.md.
indicator("KONATUS | Market Anatomy [Research 0.1]", "K Anatomy", overlay = true, max_bars_back = 1200)
// One common measurement system; horizon is always measured in chart bars.
int horizon = input.int(20, "Observation horizon (bars)", minval = 5, maxval = 200, group = "Understanding")
string volumeInput = input.string("Auto", "Volume meaning", options = ["Auto", "Trade units", "Tick count", "Quote value", "Off"], group = "Data", tooltip = "Auto follows feed metadata. Only declared trade units enable VWAP. Unknown volume stays unverified; tick activity is not traded quantity.")
string anchorInput = input.string("Auto", "VWAP calendar anchor", options = ["Auto", "Day", "Week", "Month", "Year", "Off"], group = "Data", tooltip = "Exchange-calendar boundaries on the bars loaded in the chart. Auto: intraday/day, daily/month, weekly or monthly/year. First partial anchor is withheld. This is not a regular-session filter.")
bool detailed = input.bool(false, "Detailed explanation", group = "Display")
bool showPanel = input.bool(true, "Explanation panel", group = "Display")
bool showEnvelope = input.bool(true, "Descriptive extension envelope", group = "Display", tooltip = "Filtered price +/- 1.5 prior mean true ranges. This is not a probability interval or a stop.")
bool showVwap = input.bool(true, "Volume-weighted reference", group = "Display")
bool showScenario = input.bool(true, "Frozen scenario boundary", group = "Display")
bool paintBars = input.bool(false, "Color confirmed candles", group = "Display")
string corner = input.string("Top right", "Panel position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = "Display")
f_number(float value) =>
na(value) ? "Unavailable" : str.tostring(value, "#.###")
f_row(table panel, int row, string caption, string value, color tone) =>
table.cell(panel, 0, row, caption, text_color = color.new(chart.fg_color, 30), text_size = size.small, text_halign = text.align_left)
table.cell(panel, 1, row, value, text_color = tone, text_size = size.small, text_halign = text.align_left)
color upColor = color.rgb(38, 171, 145)
color downColor = color.rgb(224, 102, 108)
color cautionColor = color.rgb(213, 158, 66)
color neutralColor = color.new(chart.fg_color, 25)
bool timedChart = timeframe.isseconds or timeframe.isminutes or timeframe.isdaily or timeframe.isweekly or timeframe.ismonthly
bool supported = chart.is_standard and timedChart
bool validPrice = supported and not na(open) and not na(high) and not na(low) and not na(close) and low <= math.min(open, close) and high >= math.max(open, close)
var int priceCount = 0
priceCount := validPrice ? priceCount + 1 : 0
bool previousValid = validPrice[1]
float trueRange = validPrice ? (previousValid ? math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) : high - low) : na
float shortRange = ta.sma(trueRange, horizon)
float longRange = ta.sma(trueRange, 4 * horizon)
float scale = shortRange[1]
bool warm = priceCount >= 4 * horizon + 2
bool scaleKnown = warm and not na(scale) and scale > 0
// Two-pole SuperSmoother in signed price space. No log-price clipping.
float decay = math.exp(-math.sqrt(2.0) * math.pi / horizon)
float c2 = 2.0 * decay * math.cos(math.sqrt(2.0) * math.pi / horizon)
float c3 = -decay * decay
float c1 = 1.0 - c2 - c3
var float level = na
float level1 = previousValid ? nz(level[1], close) : close
float level2 = previousValid ? nz(level[2], level1) : close
level := validPrice ? c1 * (close + (previousValid ? close[1] : close)) / 2.0 + c2 * level1 + c3 * level2 : na
float velocity = scaleKnown ? (level - level1) / scale : na
float acceleration = scaleKnown ? (level - 2.0 * level1 + level2) / scale : na
float curvature = scaleKnown ? acceleration / math.pow(1.0 + velocity * velocity, 1.5) : na
float extension = scaleKnown ? (close - level) / scale : na
float distance = math.sum(math.abs(close - close[1]), horizon)
float efficiency = warm ? (distance > 0 ? math.abs(close - close[horizon]) / distance : 0.0) : na
float volatility = warm and longRange > 0 ? shortRange / longRange : na
float shockRatio = scaleKnown ? trueRange / scale : na
bool shock = scaleKnown and shockRatio >= 3.0
int direction = scaleKnown ? (velocity > 0.08 ? 1 : velocity < -0.08 ? -1 : 0) : 0
// Volume semantics remain separate from directional evidence.
string volumeKind = volumeInput == "Auto" ? (syminfo.volumetype == "base" ? "Trade units" : syminfo.volumetype == "tick" ? "Tick count" : syminfo.volumetype == "quote" ? "Quote value" : "Unverified") : volumeInput
bool validVolume = validPrice and not na(volume) and volume >= 0
var int volumeCount = 0
volumeCount := validVolume ? volumeCount + 1 : 0
float volumeMean = ta.sma(validVolume ? volume : na, horizon)[1]
float relativeVolume = volumeInput != "Off" and volumeCount >= horizon + 1 and volumeMean > 0 ? volume / volumeMean : na
string anchorTf = switch anchorInput
"Day" => "1D"
"Week" => "1W"
"Month" => "1M"
"Year" => "12M"
=> timeframe.isintraday ? "1D" : timeframe.isdaily ? "1M" : "12M"
float chartSeconds = timedChart ? timeframe.in_seconds() : na
bool anchorLonger = timedChart and timeframe.in_seconds(anchorTf) > chartSeconds
bool anchorEnabled = supported and anchorInput != "Off" and volumeKind == "Trade units" and anchorLonger
bool anchorChanged = timeframe.change(anchorTf) and not barstate.isfirst
var bool anchorComplete = false
var bool anchorPoisoned = false
var float anchorWeight = 0.0
var float anchorMean = 0.0
if anchorChanged
anchorComplete := true
anchorPoisoned := false
anchorWeight := 0.0
anchorMean := 0.0
if not validVolume
anchorPoisoned := true
else if volume > 0 and anchorEnabled
float totalWeight = anchorWeight + volume
anchorMean += volume / totalWeight * (hlc3 - anchorMean)
anchorWeight := totalWeight
float vwapReference = anchorEnabled and anchorComplete and not anchorPoisoned and anchorWeight > 0 ? anchorMean : na
// A scenario freezes a previously observable boundary. It is not a trade ledger.
float priorHigh = ta.highest(high, horizon)[1]
float priorLow = ta.lowest(low, horizon)[1]
var int scenario = 0
var float boundary = na
bool invalidated = false
int breakout = 0
if not scaleKnown
scenario := 0
boundary := na
else if scenario != 0 and (close - boundary) * scenario < 0
invalidated := true
scenario := 0
boundary := na
else if scenario == 0
if close > priorHigh
scenario := 1
boundary := priorHigh
breakout := 1
else if close < priorLow
scenario := -1
boundary := priorLow
breakout := -1
// Each message is a direct explanation of a measurement or a named hypothesis.
string state = not supported ? "UNSUPPORTED CHART" : not validPrice ? "INVALID PRICE DATA" : not warm ? "BUILDING HISTORY" : not scaleKnown ? (trueRange == 0 ? "FLAT" : "SCALE UNAVAILABLE") : direction > 0 ? "UPWARD" : direction < 0 ? "DOWNWARD" : "BALANCED"
string organization = not warm ? "Awaiting history" : efficiency >= 0.55 ? "Directional path" : efficiency < 0.25 ? "Backtracking path" : "Mixed path"
float directionalAcceleration = acceleration * direction
string pace = not scaleKnown ? "Unavailable" : direction == 0 ? "No directional pace" : directionalAcceleration > 0.015 ? "Gaining pace" : directionalAcceleration < -0.015 ? "Losing pace" : "Steady pace"
string volatilityText = not warm ? "Awaiting history" : shock ? "SHOCK / " + f_number(shockRatio) + " x prior range" : na(volatility) ? "No range" : volatility > 1.3 ? "Expanding" : volatility < 0.7 ? "Contracting" : "Near longer baseline"
string locationText = not scaleKnown ? "Unavailable" : (math.abs(extension) >= 1.5 ? "Extended / " : "Near reference / ") + f_number(extension) + " range units"
string participation = volumeInput == "Off" ? "Off / price-only context" : not validVolume ? "Missing volume" : na(relativeVolume) ? "Baseline unavailable" : f_number(relativeVolume) + " x / " + volumeKind
if timeframe.isintraday and volumeInput != "Off"
participation += "\nNot time-of-day adjusted"
string anchorText = anchorInput == "Off" ? "Off" : volumeKind != "Trade units" ? "Requires declared trade units" : not anchorLonger ? "Anchor must exceed chart interval" : not anchorComplete ? "First partial anchor withheld" : anchorPoisoned ? "Anchor has missing data" : anchorWeight <= 0 ? "No positive anchor volume" : (close > vwapReference ? "Above " : close < vwapReference ? "Below " : "At ") + anchorTf + " VWAP"
string scenarioText = invalidated ? "Prior breakout failed" : scenario > 0 ? "Upward range breakout" : scenario < 0 ? "Downward range breakout" : "No active breakout scenario"
string failureText = scenario > 0 ? "Close below " + str.tostring(boundary, format.mintick) : scenario < 0 ? "Close above " + str.tostring(boundary, format.mintick) : "No scenario boundary"
string reading = not warm ? "Waiting for " + str.tostring(4 * horizon + 2) + " consecutive valid bars." : not scaleKnown ? "Normalized movement is unavailable." : shock ? "Range jumped relative to its prior baseline.\nThe previous pace may no longer describe conditions." : direction != 0 and efficiency < 0.25 ? "The smoother points one way; raw price backtracks.\nDirection and path organization conflict." : direction != 0 and math.abs(extension) >= 1.5 ? "Price is far from the smoothed reference.\nExtension alone does not imply reversal." : direction != 0 ? "The filtered path is directional.\nRead pace and participation separately." : "Directional movement is weak under this definition.\nA balanced reading does not predict a breakout."
string hypothesis = not scaleKnown ? "Insufficient normalized evidence." : shock ? "Information arrival or a liquidity change are possible.\nThe chart does not identify the cause." : direction != 0 and efficiency >= 0.55 ? "Persistent execution demand is one possible explanation.\nParticipant identity and intent are unobserved." : "Two-sided trade is one possible explanation.\nOHLCV cannot identify who supplied liquidity."
color stateColor = not warm or not scaleKnown ? neutralColor : direction > 0 ? upColor : direction < 0 ? downColor : neutralColor
// Developing bars carry no final plot/marker. The table holds the last closed bar.
bool confirmed = barstate.isconfirmed
plot(confirmed and warm ? level : na, "Filtered price", color = stateColor, linewidth = 2, style = plot.style_linebr)
upperPlot = plot(confirmed and scaleKnown and showEnvelope ? level + 1.5 * scale : na, "Upper descriptive envelope", color = color.new(neutralColor, 80), style = plot.style_linebr)
lowerPlot = plot(confirmed and scaleKnown and showEnvelope ? level - 1.5 * scale : na, "Lower descriptive envelope", color = color.new(neutralColor, 80), style = plot.style_linebr)
fill(upperPlot, lowerPlot, color = color.new(stateColor, 95), title = "Descriptive extension area")
plot(confirmed and showVwap ? vwapReference : na, "Calendar VWAP", color = color.new(cautionColor, 15), style = plot.style_linebr)
plot(confirmed and showScenario ? boundary : na, "Scenario boundary (not stop)", color = cautionColor, linewidth = 1, style = plot.style_circles)
barcolor(paintBars and confirmed and warm ? stateColor : na)
plotshape(confirmed and breakout == 1, "Upward breakout observation", shape.circle, location.belowbar, upColor, size = size.tiny)
plotshape(confirmed and breakout == -1, "Downward breakout observation", shape.circle, location.abovebar, downColor, size = size.tiny)
plotshape(confirmed and invalidated, "Scenario invalidated", shape.xcross, location.abovebar, cautionColor, size = size.tiny)
string panelPosition = corner == "Top right" ? position.top_right : corner == "Bottom right" ? position.bottom_right : corner == "Top left" ? position.top_left : position.bottom_left
var table panel = table.new(panelPosition, 2, detailed ? 18 : 12, bgcolor = color.new(chart.bg_color, 5), frame_color = color.new(chart.fg_color, 80), frame_width = 1)
if barstate.islast and showPanel
int snapshot = confirmed ? 0 : 1
color tone = stateColor[snapshot]
string clockText = na(time_close[snapshot]) ? "No confirmed bar" : str.format_time(time_close[snapshot], "MM-dd HH:mm", syminfo.timezone)
f_row(panel, 0, "KONATUS", "MARKET ANATOMY / 0.1", chart.fg_color)
f_row(panel, 1, syminfo.ticker + " / " + timeframe.period, state[snapshot], tone)
f_row(panel, 2, "Path / efficiency", organization[snapshot] + " / " + f_number(efficiency[snapshot]), tone)
f_row(panel, 3, "Pace", pace[snapshot], tone)
f_row(panel, 4, "Volatility", volatilityText[snapshot], shock[snapshot] ? cautionColor : chart.fg_color)
f_row(panel, 5, "Location", locationText[snapshot], chart.fg_color)
f_row(panel, 6, "Activity / rolling", participation[snapshot], chart.fg_color)
f_row(panel, 7, "Anchor context", anchorText[snapshot], chart.fg_color)
f_row(panel, 8, "Scenario", scenarioText[snapshot] + "\nInvalidation: " + failureText[snapshot], cautionColor)
f_row(panel, 9, "Reading", reading[snapshot], chart.fg_color)
if detailed
f_row(panel, 10, "Possible mechanism", hypothesis[snapshot], neutralColor)
f_row(panel, 11, "Velocity / acceleration", f_number(velocity[snapshot]) + " / " + f_number(acceleration[snapshot]), tone)
f_row(panel, 12, "Local curvature", f_number(curvature[snapshot]) + " / defined coordinates", neutralColor)
f_row(panel, 13, "Prior range scale", f_number(scale[snapshot]) + " / SMA true range", neutralColor)
f_row(panel, 14, "Data meaning", volumeKind + " / " + syminfo.type, neutralColor)
f_row(panel, 15, "Activity limitation", "Rolling baseline; no time-of-day adjustment.", neutralColor)
f_row(panel, detailed ? 16 : 10, "Confirmed through", clockText + " / " + syminfo.timezone, neutralColor)
f_row(panel, detailed ? 17 : 11, "Evidence", "Descriptive / parameters unvalidated", neutralColor)
// Exports enable a native TradingView versus Python fixture comparison.
plot(confirmed ? velocity : na, "Velocity", display = display.data_window)
plot(confirmed ? acceleration : na, "Acceleration", display = display.data_window)
plot(confirmed ? efficiency : na, "Efficiency", display = display.data_window)
plot(confirmed ? extension : na, "Extension", display = display.data_window)
plot(confirmed ? curvature : na, "Curvature", display = display.data_window)
plot(confirmed and warm ? volatility : na, "Volatility ratio", display = display.data_window)
plot(confirmed ? relativeVolume : na, "Relative activity", display = display.data_window)
plot(confirmed and warm ? scale : na, "Prior scale", display = display.data_window)
plot(confirmed and warm ? direction : na, "Direction", display = display.data_window)
bool directionChanged = scaleKnown and scaleKnown[1] and direction != direction[1]
alertcondition(confirmed and directionChanged, "Direction changed", "KONATUS {{ticker}} {{interval}}: confirmed direction changed. Read the explanation panel; this is not an entry order.")
alertcondition(confirmed and shock and not shock[1], "Volatility shock", "KONATUS {{ticker}} {{interval}}: confirmed range shock relative to prior history.")
alertcondition(confirmed and breakout != 0, "Range breakout observed", "KONATUS {{ticker}} {{interval}}: confirmed range breakout. A descriptive scenario boundary is now frozen.")
alertcondition(confirmed and invalidated, "Scenario invalidated", "KONATUS {{ticker}} {{interval}}: a confirmed close invalidated the frozen breakout scenario.")
Designed for valid time-based OHLC data across stocks, ETFs, futures, forex and crypto; feed and timeframe coverage still need testing. Relative activity uses a rolling baseline, without time-of-day adjustment. The range scale is an SMA of true range, not Wilder’s ATR. A frozen breakout boundary is a scenario reference, not an order or stop.
The research package includes the script, setup guide, mathematics, specification and reference tests. Back to Konatus Bearing ↑
03 / New integrated edition
Understand the conditions.
See the plan.
Bearing’s planning engine and Anatomy’s market readings, already integrated in one Pine v6 indicator. One installation, one coordinated dashboard, and optional context filters.
Get the integrated code ↓
Research release 0.1.0. 35 offline Python tests pass. Native TradingView compilation, chart behavior and alert delivery remain unverified. The optional combined policies have no established trading edge.
Direction, velocity, acceleration, curvature, path efficiency, volatility and participation retain Anatomy’s explicit definitions.
Bearing retains its setup checks, illustrative next-open ledger, sizing assumptions, frozen stops and exit logic.
A shared dashboard explains agreement and conflict. Optional policies let Anatomy withhold new Bearing candidates.
Choose compact or detailed readings, dark or light styling, text size and panel corner. One system owns candle colors. Extra Anatomy lines, the envelope and calendar VWAP are optional.
A solid gold line represents Bearing’s illustrative stop. A dotted teal line represents Anatomy’s descriptive scenario boundary. They have different jobs.
KONATUS ATLAS / 0.1
WAIT SETUP
Conditions available. Wait for setup.
RESEARCH · ILLUSTRATIVE PLANS
The filters cannot create a candidate Bearing rejected or increase its position size. A context change does not cancel an already queued illustrative fill, erase a frozen stop or suppress an exit. Changing inputs recomputes the chart history.
Download Atlas Pine v6Research package (.zip)
//@version=6
indicator("KONATUS Atlas · Integrated Research 0.1.0", "K Atlas", overlay = true, max_bars_back = 2000)
// Copyright (c) 2026 Kutty Stauder. Research software; no established trading edge.
// Built from frozen Bearing 0.3.1 and Anatomy 0.1.0 engines. See SPEC.md.
string atlasPolicy = input.string("Observe only", "Anatomy entry policy", options = ["Observe only", "Require direction", "Require organized direction"], group = "00 · Atlas", tooltip = "Observe preserves Bearing entries. Optional policies require valid Anatomy context, no Anatomy shock and matching direction. Organized also requires efficiency >= 0.55. Changes new entry candidates only; exits and queued fills remain active. These policies are unvalidated research rules.")
string atlasCorner = input.string("Top right", "Dashboard position", options = ["Top right", "Top left", "Bottom right", "Bottom left"], group = "00 · Atlas")
string atlasDensity = input.string("Compact", "Dashboard detail", options = ["Compact", "Detailed", "Hidden"], group = "00 · Atlas")
string atlasFont = input.string("Small", "Dashboard text", options = ["Small", "Tiny", "Normal"], group = "00 · Atlas")
bool atlasCandles = input.bool(true, "Color confirmed candles", group = "00 · Atlas")
bool atlasAnatomyLine = input.bool(false, "Show Anatomy filtered price", group = "00 · Atlas")
bool atlasEnvelope = input.bool(false, "Show Anatomy descriptive envelope", group = "00 · Atlas")
bool atlasVwap = input.bool(false, "Show Anatomy calendar VWAP", group = "00 · Atlas")
bool atlasScenario = input.bool(true, "Show Anatomy scenario boundary", group = "00 · Atlas")
bool atlasEvents = input.bool(false, "Show Anatomy observation markers", group = "00 · Atlas")
// === ANATOMY ENGINE ===
// One common measurement system; horizon is always measured in chart bars.
int a_horizon = input.int(20, "Observation horizon (bars)", minval = 5, maxval = 200, group = "A · Understanding")
string a_volumeInput = input.string("Auto", "Volume meaning", options = ["Auto", "Trade units", "Tick count", "Quote value", "Off"], group = "A · Volume and anchors", tooltip = "Auto follows feed metadata. Only declared trade units enable VWAP. Unknown volume stays unverified; tick activity is not traded quantity.")
string a_anchorInput = input.string("Auto", "VWAP calendar anchor", options = ["Auto", "Day", "Week", "Month", "Year", "Off"], group = "A · Volume and anchors", tooltip = "Exchange-calendar boundaries on the bars loaded in the chart. Auto: intraday/day, daily/month, weekly or monthly/year. First partial anchor is withheld. This is not a regular-session filter.")
a_f_number(float value) =>
na(value) ? "Unavailable" : str.tostring(value, "#.###")
color a_upColor = color.rgb(38, 171, 145)
color a_downColor = color.rgb(224, 102, 108)
color a_cautionColor = color.rgb(213, 158, 66)
color a_neutralColor = color.new(chart.fg_color, 25)
bool a_timedChart = timeframe.isseconds or timeframe.isminutes or timeframe.isdaily or timeframe.isweekly or timeframe.ismonthly
bool a_supported = chart.is_standard and a_timedChart
bool a_validPrice = a_supported and not na(open) and not na(high) and not na(low) and not na(close) and low <= math.min(open, close) and high >= math.max(open, close)
var int a_priceCount = 0
a_priceCount := a_validPrice ? a_priceCount + 1 : 0
bool a_previousValid = a_validPrice[1]
float a_trueRange = a_validPrice ? (a_previousValid ? math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) : high - low) : na
float a_shortRange = ta.sma(a_trueRange, a_horizon)
float a_longRange = ta.sma(a_trueRange, 4 * a_horizon)
float a_scale = a_shortRange[1]
bool a_warm = a_priceCount >= 4 * a_horizon + 2
bool a_scaleKnown = a_warm and not na(a_scale) and a_scale > 0
// Two-pole SuperSmoother in signed price space. No log-price clipping.
float a_decay = math.exp(-math.sqrt(2.0) * math.pi / a_horizon)
float a_c2 = 2.0 * a_decay * math.cos(math.sqrt(2.0) * math.pi / a_horizon)
float a_c3 = -a_decay * a_decay
float a_c1 = 1.0 - a_c2 - a_c3
var float a_level = na
float a_level1 = a_previousValid ? nz(a_level[1], close) : close
float a_level2 = a_previousValid ? nz(a_level[2], a_level1) : close
a_level := a_validPrice ? a_c1 * (close + (a_previousValid ? close[1] : close)) / 2.0 + a_c2 * a_level1 + a_c3 * a_level2 : na
float a_velocity = a_scaleKnown ? (a_level - a_level1) / a_scale : na
float a_acceleration = a_scaleKnown ? (a_level - 2.0 * a_level1 + a_level2) / a_scale : na
float a_curvature = a_scaleKnown ? a_acceleration / math.pow(1.0 + a_velocity * a_velocity, 1.5) : na
float a_extension = a_scaleKnown ? (close - a_level) / a_scale : na
float a_distance = math.sum(math.abs(close - close[1]), a_horizon)
float a_efficiency = a_warm ? (a_distance > 0 ? math.abs(close - close[a_horizon]) / a_distance : 0.0) : na
float a_volatility = a_warm and a_longRange > 0 ? a_shortRange / a_longRange : na
float a_shockRatio = a_scaleKnown ? a_trueRange / a_scale : na
bool a_shock = a_scaleKnown and a_shockRatio >= 3.0
int a_direction = a_scaleKnown ? (a_velocity > 0.08 ? 1 : a_velocity < -0.08 ? -1 : 0) : 0
// Volume semantics remain separate from directional evidence.
string a_volumeKind = a_volumeInput == "Auto" ? (syminfo.volumetype == "base" ? "Trade units" : syminfo.volumetype == "tick" ? "Tick count" : syminfo.volumetype == "quote" ? "Quote value" : "Unverified") : a_volumeInput
bool a_validVolume = a_validPrice and not na(volume) and volume >= 0
var int a_volumeCount = 0
a_volumeCount := a_validVolume ? a_volumeCount + 1 : 0
float a_volumeMean = ta.sma(a_validVolume ? volume : na, a_horizon)[1]
float a_relativeVolume = a_volumeInput != "Off" and a_volumeCount >= a_horizon + 1 and a_volumeMean > 0 ? volume / a_volumeMean : na
string a_anchorTf = switch a_anchorInput
"Day" => "1D"
"Week" => "1W"
"Month" => "1M"
"Year" => "12M"
=> timeframe.isintraday ? "1D" : timeframe.isdaily ? "1M" : "12M"
float a_chartSeconds = a_timedChart ? timeframe.in_seconds() : na
bool a_anchorLonger = a_timedChart and timeframe.in_seconds(a_anchorTf) > a_chartSeconds
bool a_anchorEnabled = a_supported and a_anchorInput != "Off" and a_volumeKind == "Trade units" and a_anchorLonger
bool a_anchorChanged = timeframe.change(a_anchorTf) and not barstate.isfirst
var bool a_anchorComplete = false
var bool a_anchorPoisoned = false
var float a_anchorWeight = 0.0
var float a_anchorMean = 0.0
if a_anchorChanged
a_anchorComplete := true
a_anchorPoisoned := false
a_anchorWeight := 0.0
a_anchorMean := 0.0
if not a_validVolume
a_anchorPoisoned := true
else if volume > 0 and a_anchorEnabled
float a_totalWeight = a_anchorWeight + volume
a_anchorMean += volume / a_totalWeight * (hlc3 - a_anchorMean)
a_anchorWeight := a_totalWeight
float a_vwapReference = a_anchorEnabled and a_anchorComplete and not a_anchorPoisoned and a_anchorWeight > 0 ? a_anchorMean : na
// A scenario freezes a previously observable boundary. It is not a trade ledger.
float a_priorHigh = ta.highest(high, a_horizon)[1]
float a_priorLow = ta.lowest(low, a_horizon)[1]
var int a_scenario = 0
var float a_boundary = na
bool a_invalidated = false
int a_breakout = 0
if not a_scaleKnown
a_scenario := 0
a_boundary := na
else if a_scenario != 0 and (close - a_boundary) * a_scenario < 0
a_invalidated := true
a_scenario := 0
a_boundary := na
else if a_scenario == 0
if close > a_priorHigh
a_scenario := 1
a_boundary := a_priorHigh
a_breakout := 1
else if close < a_priorLow
a_scenario := -1
a_boundary := a_priorLow
a_breakout := -1
// Each message is a direct explanation of a measurement or a named hypothesis.
string a_state = not a_supported ? "UNSUPPORTED CHART" : not a_validPrice ? "INVALID PRICE DATA" : not a_warm ? "BUILDING HISTORY" : not a_scaleKnown ? (a_trueRange == 0 ? "FLAT" : "SCALE UNAVAILABLE") : a_direction > 0 ? "UPWARD" : a_direction < 0 ? "DOWNWARD" : "BALANCED"
string a_organization = not a_warm ? "Awaiting history" : a_efficiency >= 0.55 ? "Directional path" : a_efficiency < 0.25 ? "Backtracking path" : "Mixed path"
float a_directionalAcceleration = a_acceleration * a_direction
string a_pace = not a_scaleKnown ? "Unavailable" : a_direction == 0 ? "No directional pace" : a_directionalAcceleration > 0.015 ? "Gaining pace" : a_directionalAcceleration < -0.015 ? "Losing pace" : "Steady pace"
string a_volatilityText = not a_warm ? "Awaiting history" : a_shock ? "SHOCK / " + a_f_number(a_shockRatio) + " x prior range" : na(a_volatility) ? "No range" : a_volatility > 1.3 ? "Expanding" : a_volatility < 0.7 ? "Contracting" : "Near longer baseline"
string a_locationText = not a_scaleKnown ? "Unavailable" : (math.abs(a_extension) >= 1.5 ? "Extended / " : "Near reference / ") + a_f_number(a_extension) + " range units"
string a_participation = a_volumeInput == "Off" ? "Off / price-only context" : not a_validVolume ? "Missing volume" : na(a_relativeVolume) ? "Baseline unavailable" : a_f_number(a_relativeVolume) + " x / " + a_volumeKind
if timeframe.isintraday and a_volumeInput != "Off"
a_participation += "\nNot time-of-day adjusted"
string a_anchorText = a_anchorInput == "Off" ? "Off" : a_volumeKind != "Trade units" ? "Requires declared trade units" : not a_anchorLonger ? "Anchor must exceed chart interval" : not a_anchorComplete ? "First partial anchor withheld" : a_anchorPoisoned ? "Anchor has missing data" : a_anchorWeight <= 0 ? "No positive anchor volume" : (close > a_vwapReference ? "Above " : close < a_vwapReference ? "Below " : "At ") + a_anchorTf + " VWAP"
string a_scenarioText = a_invalidated ? "Prior breakout failed" : a_scenario > 0 ? "Upward range breakout" : a_scenario < 0 ? "Downward range breakout" : "No active breakout scenario"
string a_failureText = a_scenario > 0 ? "Close below " + str.tostring(a_boundary, format.mintick) : a_scenario < 0 ? "Close above " + str.tostring(a_boundary, format.mintick) : "No scenario boundary"
string a_reading = not a_warm ? "Waiting for " + str.tostring(4 * a_horizon + 2) + " consecutive valid bars." : not a_scaleKnown ? "Normalized movement is unavailable." : a_shock ? "Range jumped relative to its prior baseline.\nThe previous pace may no longer describe conditions." : a_direction != 0 and a_efficiency < 0.25 ? "The smoother points one way; raw price backtracks.\nDirection and path organization conflict." : a_direction != 0 and math.abs(a_extension) >= 1.5 ? "Price is far from the smoothed reference.\nExtension alone does not imply reversal." : a_direction != 0 ? "The filtered path is directional.\nRead pace and participation separately." : "Directional movement is weak under this definition.\nA balanced reading does not predict a breakout."
string a_hypothesis = not a_scaleKnown ? "Insufficient normalized evidence." : a_shock ? "Information arrival or a liquidity change are possible.\nThe chart does not identify the cause." : a_direction != 0 and a_efficiency >= 0.55 ? "Persistent execution demand is one possible explanation.\nParticipant identity and intent are unobserved." : "Two-sided trade is one possible explanation.\nOHLCV cannot identify who supplied liquidity."
color a_stateColor = not a_warm or not a_scaleKnown ? a_neutralColor : a_direction > 0 ? a_upColor : a_direction < 0 ? a_downColor : a_neutralColor
// === BEARING ENGINE ===
// Copyright (c) 2026 Kontaus. Experimental kb-0.3; no established market edge.
bool b_allowShort = input.bool(false, "Enable short entries", group = "B · 00 · Entry direction", tooltip = "Long entries are enabled. Turn this on to also allow red short-entry triangles and short plans. Signals still require all setup and risk conditions. This control is in the cross-market 0.3.1 indicator; the old 0.2 baseline is long-only.")
string b_profileInput = input.string("Auto", "Instrument profile", options = ["Auto", "Stocks", "Bonds", "Futures", "Forex", "Crypto", "Generic"], group = "B · 01 · Market")
string b_profile = b_profileInput == "Auto" ? (syminfo.type == "stock" or syminfo.type == "fund" ? "Stocks" : syminfo.type == "futures" ? "Futures" : syminfo.type == "forex" ? "Forex" : syminfo.type == "crypto" ? "Crypto" : "Generic") : b_profileInput
string b_volumePolicy = input.string("Profile", "Participation policy", options = ["Profile", "Required", "Price only"], group = "B · 01 · Market", tooltip = "Profile requires volume for Stocks/Futures/Crypto. Bonds/Forex/Generic use price only. Missing required volume blocks new plans.")
string b_volumeMode = input.string("Prior EW", "Volume baseline", options = ["Prior EW", "Session slot"], group = "B · 01 · Market", tooltip = "Prior EW uses 20 earlier volume observations. Session slot uses at least 5 earlier trading days at this exchange-local minute. Minute or hourly charts only; no current-bar volume in the baseline.")
bool b_vwapGate = input.bool(false, "Require session VWAP alignment", group = "B · 01 · Market", tooltip = "Requires a fully observed trading-day anchor and positive current volume. VWAP is context, not order flow.")
int b_resetAt = input.time(0, "Reset model at contract/data boundary", group = "B · 01 · Market", tooltip = "Optional timestamp. Resets analytical history once; never cancels an open illustrative stop. Does not construct or adjust a futures roll.")
string b_setupMode = input.string("Breakout", "Setup", options = ["Breakout", "Pullback", "Either"], group = "B · 02 · Signals")
float b_decay = input.float(0.9, "Evidence memory", minval = 0.01, maxval = 0.99, step = 0.01, group = "B · 02 · Signals")
float b_persistenceMin = input.float(20, "Minimum directional evidence", minval = 0, maxval = 100, group = "B · 02 · Signals")
float b_extensionLimit = input.float(1.5, "Maximum entry extension · ATR", minval = 0.1, group = "B · 02 · Signals")
float b_efficiencyMin = input.float(0.25, "Minimum directional efficiency", minval = 0, maxval = 1, group = "B · 02 · Signals")
float b_shockAtr = input.float(4, "Shock threshold · prior ATR", minval = 0.1, group = "B · 02 · Signals")
int b_cooldown = input.int(3, "Shock cooldown · bars", minval = 0, maxval = 100, group = "B · 02 · Signals")
float b_account = input.float(100000, "Reference account", minval = 1, group = "B · 03 · Risk")
float b_riskPct = input.float(1, "Planned risk %", minval = 0, maxval = 100, group = "B · 03 · Risk")
float b_stopAtr = input.float(3, "Frozen distance · ATR", minval = 0.1, group = "B · 03 · Risk")
float b_conversion = input.float(1, "Account currency per quote currency", minval = 0.00000001, group = "B · 03 · Risk", tooltip = "Explicit fixed conversion for the illustration, not an FX feed. Use 1 only when account and quote currency match. Recompute when conversion changes.")
float b_pointOverride = input.float(0, "Point-value override · 0 = symbol", minval = 0, group = "B · 03 · Risk")
float b_pointValue = b_pointOverride > 0 ? b_pointOverride : syminfo.pointvalue
float b_tick = syminfo.mintick
float b_lot = input.float(1, "Quantity increment", minval = 0.00000001, group = "B · 03 · Risk", tooltip = "Quantity uses symbol units/contracts, not broker-defined FX lots. Confirm the point value and increment for this instrument.")
float b_margin = input.float(0, "Capacity per unit · account currency", minval = 0, group = "B · 03 · Risk", tooltip = "Required for Futures and nonpositive-price plans. Enter a conservative per-contract allocation. It is not a live broker margin model. Zero uses cash notional for other positive-price instruments.")
float b_capitalFraction = input.float(1, "Maximum capacity · account fraction", minval = 0, maxval = 1, group = "B · 03 · Risk")
float b_costBps = input.float(5, "One-way notional costs · bps", minval = 0, group = "B · 03 · Risk")
float b_feePerUnit = input.float(0, "One-way fee per unit · quote currency", minval = 0, group = "B · 03 · Risk")
int b_slipTicks = input.int(1, "Adverse slippage · ticks", minval = 0, maxval = 100, group = "B · 03 · Risk")
string b_theme = input.string("Auto", "Theme", options = ["Auto", "Dark", "Light"], group = "B · 04 · Appearance")
if not chart.is_standard or timeframe.isticks
runtime.error("Konatus requires standard time-based OHLC charts.")
if na(b_pointValue) or na(b_tick) or math.sign(b_pointValue) <= 0 or math.sign(b_tick) <= 0
runtime.error("A positive tick and point value are required.")
if b_volumeMode == "Session slot" and (not timeframe.isintraday or timeframe.in_seconds() < 60)
runtime.error("Session-slot volume needs intraday time bars of at least one minute.")
bool b_needsVolume = b_volumePolicy == "Required" or (b_volumePolicy == "Profile" and (b_profile == "Stocks" or b_profile == "Futures" or b_profile == "Crypto"))
if b_vwapGate and not timeframe.isintraday
runtime.error("Session VWAP gating requires intraday time bars. Disable it on daily and higher charts.")
// Shared confirmed-bar analytical engine. Price units allow zero/negative prices.
var float b_level = na
var float b_drift = 0
var float b_p00 = 0
var float b_p01 = 0
var float b_p11 = 0
var float b_atr = 0
var float b_trSum = 0
var float b_previous = na
var float b_evidence = 0
var int b_count = 0
var int b_direction = 0
var int b_runLength = 0
var int b_pause = 0
var array<float> b_highs = array.new_float()
var array<float> b_lows = array.new_float()
var array<float> b_closes = array.new_float()
var array<float> b_slotMean = array.new_float(1440, 0)
var array<int> b_slotCount = array.new_int(1440, 0)
var array<int> b_slotAnchor = array.new_int(1440, na)
var float b_volMean = 0
var int b_volCount = 0
var int b_anchor = na
var bool b_anchorComplete = false
var bool b_anchorValid = true
var float b_pv = 0
var float b_vSum = 0
var float b_vwap = na
var float b_relativeVolume = na
var float b_efficiency = 0
var float b_extension = na
var float b_surprise = na
var float b_predictiveSd = na
var float b_distance = 0
var float b_quantity = 0
var string b_volumeStatus = "WARMUP"
var string b_condition = "WARMUP"
var string b_entryStatus = "WARMUP"
var string b_setupName = "NONE"
var int b_breakoutSide = 0
var float b_breakoutLevel = na
var bool b_ready = false
int b_signal = 0
bool b_valid = not na(open) and not na(high) and not na(low) and not na(close) and (math.min(open, close) - low) / b_tick >= 0 and (high - math.max(open, close)) / b_tick >= 0
bool b_resetNow = b_resetAt > 0 and time >= b_resetAt and (na(time[1]) or time[1] < b_resetAt)
if barstate.isconfirmed
if not b_valid or b_resetNow
b_level := na
b_drift := 0
b_p00 := 0
b_p01 := 0
b_p11 := 0
b_atr := 0
b_trSum := 0
b_previous := na
b_evidence := 0
b_count := 0
b_direction := 0
b_runLength := 0
b_pause := 0
array.clear(b_highs)
array.clear(b_lows)
array.clear(b_closes)
array.fill(b_slotMean, 0)
array.fill(b_slotCount, 0)
array.fill(b_slotAnchor, na)
b_volMean := 0
b_volCount := 0
b_anchor := na
b_anchorComplete := false
b_anchorValid := true
b_pv := 0
b_vSum := 0
b_vwap := na
b_breakoutSide := 0
b_ready := false
b_quantity := 0
b_distance := 0
b_extension := na
b_efficiency := 0
b_surprise := na
b_predictiveSd := na
b_relativeVolume := na
b_volumeStatus := "UNAVAILABLE"
b_condition := "INVALID DATA"
b_entryStatus := "INVALID DATA"
b_setupName := "NONE"
if b_valid
float b_priorLevel = b_level
float b_tr = na(b_previous) ? high - low : math.max(high - low, math.abs(high - b_previous), math.abs(low - b_previous))
float b_scale = math.max(b_count >= 14 ? b_atr : b_count > 0 ? b_trSum / b_count : b_tr, b_tick)
float b_variance = b_scale * b_scale
float b_observation = 0.25 * b_variance
if na(b_level)
b_level := close
b_p00 := b_observation
b_p11 := b_observation
b_surprise := 0
b_predictiveSd := math.sqrt(2 * b_observation)
else
float b_a = b_p00 + 2 * b_p01 + b_p11 + 0.01 * b_variance
float b_b = b_p01 + b_p11
float b_d = b_p11 + 0.0001 * b_variance
float b_predictive = b_a + b_observation
float b_innovation = close - b_level - b_drift
b_surprise := b_innovation / math.sqrt(b_predictive)
b_predictiveSd := math.sqrt(b_predictive)
float b_k0 = b_a / b_predictive
float b_k1 = b_b / b_predictive
float b_m = 1 - b_k0
b_level += b_drift + b_k0 * b_innovation
b_drift += b_k1 * b_innovation
b_p00 := b_m * b_m * b_a + b_k0 * b_k0 * b_observation
b_p01 := b_m * (b_b - b_k1 * b_a) + b_k0 * b_k1 * b_observation
b_p11 := b_d - 2 * b_k1 * b_b + b_k1 * b_k1 * b_a + b_k1 * b_k1 * b_observation
b_count += 1
if b_count <= 14
b_trSum += b_tr
b_atr := b_trSum / b_count
else
b_atr += (b_tr - b_atr) / 14
int b_priorDirection = b_direction
b_direction := b_drift / b_tick > 0.000001 ? 1 : b_drift / b_tick < -0.000001 ? -1 : 0
b_runLength := b_direction != 0 and b_direction == b_priorDirection ? b_runLength + 1 : b_direction != 0 ? 1 : 0
b_evidence := b_decay * b_evidence + (1 - b_decay) * math.max(-1, math.min(1, b_drift / (0.1 * b_scale)))
array.push(b_closes, close)
if array.size(b_closes) > 21
array.shift(b_closes)
float b_travel = 0
if array.size(b_closes) > 1
for j = 1 to array.size(b_closes) - 1
b_travel += math.abs(array.get(b_closes, j) - array.get(b_closes, j - 1))
b_efficiency := b_travel > 0 ? math.abs(close - array.get(b_closes, 0)) / b_travel : 0
b_extension := math.abs(close - b_level) / math.max(b_atr, b_tick)
bool b_shock = not na(b_previous) and math.abs(close - b_previous) / b_scale > b_shockAtr
b_pause := b_shock ? b_cooldown : math.max(0, b_pause - 1)
b_ready := b_count >= 40 and b_atr / b_tick > 0.000001
b_condition := b_count < 40 ? "WARMUP" : not b_ready ? "FLAT" : b_shock or b_pause > 0 ? "SHOCK" : b_efficiency < b_efficiencyMin ? "CONGESTION" : "TREND"
bool b_haveVolume = not na(volume) and volume >= 0
int b_slot = hour(time, syminfo.timezone) * 60 + minute(time, syminfo.timezone)
int b_dayAnchor = time_tradingday
float b_baseline = b_volumeMode == "Session slot" ? array.get(b_slotMean, b_slot) : b_volMean
int b_samples = b_volumeMode == "Session slot" ? array.get(b_slotCount, b_slot) : b_volCount
int b_lastAnchor = array.get(b_slotAnchor, b_slot)
bool b_duplicateSlot = b_volumeMode == "Session slot" and not na(b_lastAnchor) and b_lastAnchor == b_dayAnchor
bool b_enough = b_samples >= (b_volumeMode == "Session slot" ? 5 : 20)
b_relativeVolume := b_haveVolume and b_enough and b_baseline > 0 and not b_duplicateSlot ? volume / b_baseline : na
b_volumeStatus := not b_haveVolume ? "VOLUME MISSING" : b_duplicateSlot ? "SLOT REPEATED" : volume == 0 ? "ZERO VOLUME" : na(b_relativeVolume) ? "VOLUME WARMUP" : "AVAILABLE"
if b_haveVolume and not b_duplicateSlot
float b_updated = b_samples == 0 ? volume : b_baseline + (volume - b_baseline) * 2 / 21
if b_volumeMode == "Session slot"
array.set(b_slotMean, b_slot, b_updated)
array.set(b_slotCount, b_slot, b_samples + 1)
array.set(b_slotAnchor, b_slot, b_dayAnchor)
else
b_volMean := b_updated
b_volCount += 1
if not b_needsVolume
b_volumeStatus := "PRICE ONLY"
bool b_volumeOk = not b_needsVolume or (b_volumeStatus == "AVAILABLE" and b_relativeVolume >= 1)
if na(b_anchor) or b_dayAnchor != b_anchor
b_anchorComplete := not na(b_anchor)
b_anchor := b_dayAnchor
b_anchorValid := true
b_pv := 0
b_vSum := 0
if not b_haveVolume
b_anchorValid := false
if b_haveVolume and volume > 0
b_pv += hlc3 * volume
b_vSum += volume
b_vwap := b_vSum > 0 ? b_pv / b_vSum : na
bool b_contextOk = not b_vwapGate or (b_anchorValid and b_anchorComplete and not na(b_vwap) and b_haveVolume and volume > 0 and b_direction * (close - b_vwap) / b_tick > 0)
int b_fire = 0
if not b_ready or b_shock or b_pause > 0
b_breakoutSide := 0
else if b_breakoutSide != 0
if b_breakoutSide * (close - b_breakoutLevel) / b_tick > 0
b_fire := b_breakoutSide
b_breakoutSide := 0
else if array.size(b_highs) == 20
if (close - array.max(b_highs)) / b_tick > 0
b_breakoutSide := 1
b_breakoutLevel := array.max(b_highs)
else if (array.min(b_lows) - close) / b_tick > 0
b_breakoutSide := -1
b_breakoutLevel := array.min(b_lows)
int b_pull = not na(b_previous) and not na(b_priorLevel) and b_direction * (b_previous - b_priorLevel) / b_tick <= 0 and b_direction * (close - b_level) / b_tick > 0 ? b_direction : 0
int b_setupSide = b_setupMode == "Breakout" ? b_fire : b_setupMode == "Pullback" ? b_pull : b_fire != 0 ? b_fire : b_pull
b_setupName := b_setupSide != 0 and b_setupSide == b_fire ? "BREAKOUT" : b_setupSide != 0 ? "PULLBACK" : "NONE"
b_distance := math.ceil(b_stopAtr * b_atr / b_tick - 1e-9) * b_tick
float b_slip = b_slipTicks * b_tick
float b_fees = 2 * b_feePerUnit + (2 * math.abs(close) + b_distance + 2 * b_slip) * b_pointValue * b_costBps / 10000
float b_unitRisk = ((b_distance + b_slip + b_tick) * b_pointValue + b_fees) * b_conversion
float b_capacity = b_profile == "Futures" or math.sign(close) <= 0 ? b_margin : b_margin > 0 ? b_margin : (math.abs(close) + b_slip) * b_pointValue * b_conversion
float b_capacityWithFees = b_capacity + (b_feePerUnit + (math.abs(close) + b_slip) * b_pointValue * b_costBps / 10000) * b_conversion
b_quantity := math.sign(b_capacity) > 0 and math.sign(b_distance) > 0 and math.sign(b_unitRisk) > 0 ? math.max(0, math.floor(math.min(b_account * b_riskPct / 100 / b_unitRisk, b_account * b_capitalFraction / b_capacityWithFees) / b_lot) * b_lot) : 0
b_entryStatus := b_count < 40 ? "WARMUP" : not b_ready ? "FLAT" : b_condition == "SHOCK" ? "SHOCK PAUSE" : b_condition == "CONGESTION" ? "CONGESTION" : b_direction == 0 or b_direction * b_evidence * 100 < b_persistenceMin ? "WEAK EVIDENCE" : b_direction < 0 and not b_allowShort ? "SHORT DISABLED" : not b_volumeOk and b_volumeStatus != "AVAILABLE" ? b_volumeStatus : not b_volumeOk ? "LOW PARTICIPATION" : not b_contextOk ? "VWAP CONTEXT" : b_extension > b_extensionLimit ? "EXTENDED" : b_quantity <= 0 ? "SIZE UNAVAILABLE" : b_setupSide == b_direction ? "READY" : "WAIT SETUP"
b_signal := b_entryStatus == "READY" ? b_direction : 0
array.push(b_highs, high)
array.push(b_lows, low)
if array.size(b_highs) > 20
array.shift(b_highs)
array.shift(b_lows)
b_previous := close
// Integration changes candidate entries only; existing plans keep their original ledger.
var string atlasGateStatus = "OBSERVE ONLY"
var bool atlasBlocked = false
if barstate.isconfirmed
atlasBlocked := false
atlasGateStatus := atlasPolicy == "Observe only" ? "OBSERVE ONLY" : not a_scaleKnown ? "ANATOMY WARMUP / DATA" : a_shock ? "ANATOMY SHOCK" : a_direction == 0 or a_direction != b_direction ? "DIRECTION CONFLICT" : atlasPolicy == "Require organized direction" and (na(a_efficiency) or a_efficiency < 0.55) ? "PATH NOT ORGANIZED" : "CONTEXT ALIGNED"
if atlasPolicy != "Observe only" and b_signal != 0 and atlasGateStatus != "CONTEXT ALIGNED"
b_signal := 0
b_entryStatus := atlasGateStatus
atlasBlocked := true
// Illustrative next-open ledger. Orders are not sent to a broker.
var int b_heldSide = 0
var int b_pendingSide = 0
var float b_pendingQuantity = 0
var float b_pendingDistance = 0
var float b_heldQuantity = 0
var float b_entryPrice = na
var float b_stopPrice = na
var float b_heldDistance = na
var int b_reversalCount = 0
var bool b_pendingExit = false
var bool b_capacityBreached = false
bool b_entryIntent = false
bool b_exitIntent = false
bool b_entered = false
bool b_exited = false
string b_exitReason = ""
if barstate.isconfirmed and b_valid
bool b_dueExit = b_pendingExit
b_pendingExit := false
float b_slip = b_slipTicks * b_tick
if b_heldSide != 0
if b_heldSide * (open - b_stopPrice) / b_tick <= 0
b_exited := true
b_exitReason := "GAP_STOP"
else if b_dueExit
b_exited := true
b_exitReason := "REVERSAL"
if b_heldSide == 0 and b_pendingSide != 0
b_heldSide := b_pendingSide
b_heldQuantity := b_pendingQuantity
b_heldDistance := b_pendingDistance
b_entryPrice := open + b_heldSide * b_slip
float b_rawStop = b_entryPrice - b_heldSide * b_heldDistance
b_stopPrice := (b_heldSide > 0 ? math.floor(b_rawStop / b_tick + 1e-9) : math.ceil(b_rawStop / b_tick - 1e-9)) * b_tick
float b_entryFee = b_heldQuantity * (b_feePerUnit + math.abs(b_entryPrice) * b_pointValue * b_costBps / 10000) * b_conversion
float b_filledCapacity = b_margin > 0 ? b_margin : math.abs(b_entryPrice) * b_pointValue * b_conversion
b_capacityBreached := b_filledCapacity * b_heldQuantity + b_entryFee > b_account * b_capitalFraction
b_pendingSide := 0
b_entered := true
if b_heldSide != 0 and not b_exited and (b_heldSide > 0 ? (b_stopPrice - low) / b_tick >= 0 : (high - b_stopPrice) / b_tick >= 0)
b_exited := true
b_exitReason := "STOP"
if b_exited
b_heldSide := 0
b_reversalCount := 0
if b_heldSide != 0
b_reversalCount := b_direction == -b_heldSide ? b_reversalCount + 1 : 0
if b_reversalCount >= 2
b_pendingExit := true
b_exitIntent := true
else if not b_exited and b_signal != 0
b_pendingSide := b_signal
b_pendingQuantity := b_quantity
b_pendingDistance := b_distance
b_entryIntent := true
// Alerts are decision events. Namespace separates configurations/alert instances.
string b_alertNamespace = input.string("default", "Alert instance ID", group = "B · 05 · Alerts", tooltip = "Use a distinct ID for each configuration. Allowed characters: letters, numbers, dash and underscore. Recreate TradingView alerts after settings change.")
if str.length(b_alertNamespace) == 0 or str.match(b_alertNamespace, "^[A-Za-z0-9_-]+$") != b_alertNamespace
runtime.error("Alert instance ID must contain only letters, numbers, dash or underscore.")
if b_entryIntent or b_exitIntent
string b_eventName = b_entryIntent ? "ENTRY_INTENT" : "EXIT_INTENT"
int b_eventSide = b_entryIntent ? b_pendingSide : b_heldSide
string b_eventId = "atlas-0.1:" + b_alertNamespace + ":" + syminfo.tickerid + ":" + timeframe.period + ":" + str.tostring(time_close) + ":" + b_eventName + ":" + str.tostring(b_eventSide)
string b_payload = '{"version":"atlas-0.1-experimental","id":"' + b_eventId + '","event":"' + b_eventName + '","symbol":"' + syminfo.tickerid + '","timeframe":"' + timeframe.period + '","time_close":' + str.tostring(time_close) + ',"side":' + str.tostring(b_eventSide) + ',"quantity":' + str.tostring(b_entryIntent ? b_pendingQuantity : b_heldQuantity, "#.########") + ',"distance":' + str.tostring(b_entryIntent ? b_pendingDistance : b_heldDistance, "#.########") + ',"illustrative":true}'
alert(b_payload, alert.freq_once_per_bar_close)
// === ATLAS PRESENTATION: one dashboard, one candle-color owner ===
bool atlasConfirmed = barstate.isconfirmed
bool atlasDark = b_theme == "Dark" or (b_theme == "Auto" and color.r(chart.bg_color) + color.g(chart.bg_color) + color.b(chart.bg_color) < 384)
color atlasSurface = atlasDark ? #101B24 : #F8F6EF
color atlasInk = atlasDark ? #F3EAD6 : #202D35
color atlasMuted = atlasDark ? #A7B9BD : #4C6069
color atlasUp = atlasDark ? #63D4B7 : #096A55
color atlasDown = atlasDark ? #F1A394 : #9B322C
color atlasGold = atlasDark ? #E5C078 : #755308
color atlasBorder = atlasDark ? #34464C : #B6C4C2
color atlasBearingColor = not b_ready or math.abs(b_evidence * 100) < b_persistenceMin ? atlasMuted : b_direction > 0 ? atlasUp : atlasDown
color atlasAnatomyColor = a_direction > 0 ? atlasUp : a_direction < 0 ? atlasDown : atlasMuted
bool atlasDisplayValid = atlasConfirmed ? b_valid : b_valid[1]
string atlasState = not atlasDisplayValid ? "DATA UNAVAILABLE" : b_pendingExit ? "EXIT QUEUED" : b_heldSide != 0 ? (b_heldSide > 0 ? "LONG PLAN" : "SHORT PLAN") : b_pendingSide != 0 ? "ENTRY QUEUED" : b_entryStatus
string atlasReason = b_heldSide != 0 ? (not atlasDisplayValid ? "Data invalid; last stop retained" : b_capacityBreached ? "Capacity exceeded at illustrative fill" : "Illustrative stop remains active") : b_pendingSide != 0 ? "Illustrative next-open fill queued" : atlasBlocked ? "Anatomy policy blocked this entry candidate" : b_entryStatus == "EXTENDED" ? "Bearing: price exceeds entry extension limit" : b_entryStatus == "SIZE UNAVAILABLE" ? "Review contract, capacity and risk inputs" : b_entryStatus == "WAIT SETUP" ? "Bearing conditions available; wait for setup" : "Read conditions before interpreting a setup"
string atlasPosition = atlasCorner == "Top right" ? position.top_right : atlasCorner == "Top left" ? position.top_left : atlasCorner == "Bottom right" ? position.bottom_right : position.bottom_left
string atlasTextSize = atlasFont == "Tiny" ? size.tiny : atlasFont == "Normal" ? size.normal : size.small
bool atlasDetailed = atlasDensity == "Detailed"
var table atlasPanel = table.new(atlasPosition, 2, atlasDetailed ? 23 : 14, bgcolor = atlasSurface, frame_color = atlasBorder, frame_width = 1)
atlas_row(int n, string caption, string value, color tone) =>
table.cell(atlasPanel, 0, n, " " + caption + " ", text_color = atlasMuted, bgcolor = atlasSurface, text_size = atlasTextSize, text_halign = text.align_left)
table.cell(atlasPanel, 1, n, " " + value + " ", text_color = tone, bgcolor = atlasSurface, text_size = atlasTextSize, text_halign = text.align_right)
if barstate.isfirst
table.merge_cells(atlasPanel, 0, 1, 1, 1)
table.merge_cells(atlasPanel, 0, 2, 1, 2)
table.merge_cells(atlasPanel, 0, atlasDetailed ? 22 : 13, 1, atlasDetailed ? 22 : 13)
if barstate.islast
table.clear(atlasPanel, 0, 0, 1, atlasDetailed ? 22 : 13)
if atlasDensity != "Hidden" or (b_heldSide != 0 and (b_capacityBreached or not atlasDisplayValid))
int snap = atlasConfirmed ? 0 : 1
bool anatomyAvailable = a_scaleKnown[snap]
bool agreement = anatomyAvailable and b_ready and b_direction != 0 and b_direction == a_direction[snap]
string alignment = not anatomyAvailable or not b_ready ? "Awaiting valid history" : b_direction == 0 or a_direction[snap] == 0 ? "At least one is balanced" : agreement ? "Directions agree / shared data" : "Directions disagree"
string clockText = na(time_close[snap]) ? "No confirmed bar" : str.format_time(time_close[snap], "MM-dd HH:mm", syminfo.timezone)
atlas_row(0, "KONATUS ATLAS", syminfo.ticker + " / " + timeframe.period, atlasGold)
table.cell(atlasPanel, 0, 1, " " + atlasState + " ", text_color = b_heldSide != 0 ? atlasBearingColor : atlasGold, bgcolor = atlasSurface, text_size = atlasFont == "Tiny" ? size.normal : size.large, text_halign = text.align_left)
table.cell(atlasPanel, 0, 2, " " + atlasReason + " ", text_color = atlasMuted, bgcolor = atlasSurface, text_size = atlasTextSize, text_halign = text.align_left)
atlas_row(3, "BEARING / ANATOMY", (b_direction > 0 ? "Up" : b_direction < 0 ? "Down" : "Balanced") + " / " + a_state[snap], atlasInk)
atlas_row(4, "ENTRY POLICY", atlasGateStatus, atlasGateStatus == "CONTEXT ALIGNED" ? atlasUp : atlasGold)
atlas_row(5, "PACE / RAW PATH", a_pace[snap] + " / " + a_organization[snap], atlasInk)
atlas_row(6, "ANATOMY VOLATILITY", a_volatilityText[snap], a_shock[snap] ? atlasGold : atlasInk)
atlas_row(7, "BEARING NEW ENTRY", b_entryStatus, atlasInk)
atlas_row(8, "ILLUSTRATIVE STOP", b_heldSide != 0 ? str.tostring(b_stopPrice, format.mintick) : "No open plan", atlasGold)
atlas_row(9, "ACTIVE / QUEUED QTY", str.tostring(b_heldSide != 0 ? b_heldQuantity : b_pendingSide != 0 ? b_pendingQuantity : 0, "#.########"), atlasInk)
atlas_row(10, "ALIGNMENT", alignment, agreement ? atlasUp : atlasMuted)
atlas_row(11, "ACTIVITY / ROLLING", a_participation[snap], atlasMuted)
atlas_row(12, "CONFIRMED THROUGH", clockText + " / " + syminfo.timezone, atlasMuted)
if atlasDetailed
atlas_row(13, "ANATOMY READING", a_reading[snap], atlasInk)
atlas_row(14, "POSSIBLE MECHANISM", a_hypothesis[snap], atlasMuted)
atlas_row(15, "VELOCITY / ACCELERATION", a_f_number(a_velocity[snap]) + " / " + a_f_number(a_acceleration[snap]), atlasInk)
atlas_row(16, "CURVATURE / EFFICIENCY", a_f_number(a_curvature[snap]) + " / " + a_f_number(a_efficiency[snap]), atlasInk)
atlas_row(17, "ANATOMY LOCATION", a_locationText[snap], atlasInk)
atlas_row(18, "BEARING EXTENSION", str.tostring(b_extension, "0.00") + " ATR", atlasInk)
atlas_row(19, "SCENARIO / NOT A STOP", a_scenarioText[snap] + "\n" + a_failureText[snap], atlasMuted)
atlas_row(20, "ANATOMY ANCHOR", a_anchorText[snap], atlasMuted)
atlas_row(21, "BEARING PARTICIPATION", b_volumeStatus, atlasMuted)
table.cell(atlasPanel, 0, atlasDetailed ? 22 : 13, " ATLAS 0.1 / RESEARCH / ILLUSTRATIVE PLANS ", text_color = atlasMuted, bgcolor = atlasSurface, text_size = size.tiny)
// Restrain overlays. Gold = illustrative stop; dotted teal = descriptive scenario.
plot(b_level, "Bearing price", color = atlasBearingColor, linewidth = 2)
plot(atlasConfirmed and atlasAnatomyLine and a_warm ? a_level : na, "Anatomy filtered price", color = atlasMuted)
atlasUpper = plot(atlasConfirmed and atlasEnvelope and a_scaleKnown ? a_level + 1.5 * a_scale : na, "Anatomy upper envelope", color = color.new(atlasMuted, 80))
atlasLower = plot(atlasConfirmed and atlasEnvelope and a_scaleKnown ? a_level - 1.5 * a_scale : na, "Anatomy lower envelope", color = color.new(atlasMuted, 80))
fill(atlasUpper, atlasLower, color = color.new(atlasAnatomyColor, 94), title = "Anatomy descriptive range")
plot(atlasConfirmed and atlasVwap ? a_vwapReference : na, "Anatomy calendar VWAP", color = atlasMuted)
plot(atlasConfirmed and atlasScenario ? a_boundary : na, "Anatomy scenario / NOT stop", color = atlasUp, style = plot.style_circles)
plot(b_heldSide != 0 ? b_entryPrice : na, "Illustrative entry", color = atlasMuted, style = plot.style_linebr)
plot(b_heldSide != 0 ? b_stopPrice : na, "Bearing illustrative frozen stop", color = atlasGold, linewidth = 2, style = plot.style_linebr)
barcolor(atlasCandles and atlasConfirmed ? atlasBearingColor : na)
plotshape(b_entryIntent and b_pendingSide == 1, "Long plan intent", shape.triangleup, location.belowbar, atlasUp, size = size.tiny)
plotshape(b_entryIntent and b_pendingSide == -1, "Short plan intent", shape.triangledown, location.abovebar, atlasDown, size = size.tiny)
plotshape(b_exited, "Illustrative plan exit", shape.xcross, location.abovebar, atlasGold, size = size.tiny)
plotshape(atlasEvents and atlasConfirmed and a_breakout != 0, "Anatomy breakout observation", shape.circle, location.belowbar, atlasMuted, size = size.tiny)
// Selected Data Window diagnostics, deliberately within the plot budget.
plot(atlasConfirmed ? a_velocity : na, "Anatomy velocity", display = display.data_window)
plot(atlasConfirmed ? a_acceleration : na, "Anatomy acceleration", display = display.data_window)
plot(atlasConfirmed ? a_curvature : na, "Anatomy curvature", display = display.data_window)
plot(atlasConfirmed and a_warm ? a_efficiency : na, "Anatomy raw efficiency", display = display.data_window)
plot(atlasConfirmed ? a_scale : na, "Anatomy prior SMA range", display = display.data_window)
plot(atlasConfirmed ? a_relativeVolume : na, "Anatomy rolling activity", display = display.data_window)
plot(b_evidence * 100, "Bearing signed persistence", display = display.data_window)
plot(b_atr, "Bearing ATR", display = display.data_window)
plot(b_signal, "Atlas filtered candidate", display = display.data_window)
plot(b_heldSide, "Illustrative held side", display = display.data_window)
plot(b_quantity, "Bearing candidate quantity", display = display.data_window)
plot(atlasBlocked ? 1 : 0, "Atlas context blocked candidate", display = display.data_window)
bool atlasDirectionChanged = a_scaleKnown and a_scaleKnown[1] and a_direction != a_direction[1]
alertcondition(atlasConfirmed and atlasDirectionChanged, "Atlas: Anatomy direction changed", "ATLAS {{ticker}} {{interval}}: confirmed Anatomy direction changed; not an order.")
alertcondition(atlasConfirmed and a_shock and not a_shock[1], "Atlas: Anatomy volatility shock", "ATLAS {{ticker}} {{interval}}: new Anatomy range shock.")
alertcondition(atlasConfirmed and a_breakout != 0, "Atlas: Anatomy breakout observed", "ATLAS {{ticker}} {{interval}}: descriptive range breakout, not a plan entry.")
alertcondition(atlasConfirmed and a_invalidated, "Atlas: Anatomy scenario invalidated", "ATLAS {{ticker}} {{interval}}: descriptive scenario invalidated, not a plan exit.")
alertcondition(atlasConfirmed and atlasBlocked, "Atlas: candidate blocked by context", "ATLAS {{ticker}} {{interval}}: Anatomy entry policy blocked a Bearing candidate.")
The package includes frozen engine sources, the reproducible builder, reference tests and a native-verification checklist. Atlas is an integration of your KONATUS tools, not a claim to outperform every other indicator.
04 / Upgraded · 5-minute multitrend · v0.2
The five-minute chart.
The bigger trend.
Read the local move against two higher timeframes. Adaptive structure, directional strength and volatility filters explain when a trend is strong, weak, conflicted or worth waiting out.
Get Flow 0.2 for TradingView ↓
Fast and structural adaptive averages check price location, slope and local direction.
The last completed 15-minute bar checks whether the surrounding trend agrees.
The last completed hourly bar must also agree before a trend can qualify as strong.
Coloring is now on by default. Strong and weak are defined by the filter rules, not by the size of a candle or a promised outcome. The explanation panel names the first condition holding back a strong reading.
Every upward filter agrees.
Upward structure; a filter is still waiting.
Every downward filter agrees.
Downward structure; a filter is still waiting.
A higher timeframe opposes the local trend.
A range shock calls for a cooling period.
Local structure does not establish a direction.
An open candle keeps its native chart color. Unsupported charts and incomplete or stale data are left unpainted; the panel explains why. The lines and panel hold the last closed reading. Colors are editable in Settings.
| Filter | Balanced · default | Strict |
|---|---|---|
| Minimum efficiency | 0.30 | 0.40 |
| Minimum ADX | 20 | 25 |
| Directional DI spread | 5 | 8 |
| Maximum extension | 1.5 × mean range | 1.0 × mean range |
Strict admits fewer candidates; it has not been shown to produce better returns. These are unoptimized research settings. More filters add delay and can still whipsaw.
Higher-timeframe inputs use previous completed bars. An hourly bar ending at 10:00 first contributes to the closed 10:00–10:05 candle; it is not used on the 09:55–10:00 candle. Context older than 30 minutes for 15m or 120 minutes for 1h is blocked. After a session gap, Flow may wait for new context and withhold the opening portion of the session.
Fast KAMA uses ER10 and fast/slow constants 2/30; structural KAMA uses ER30 and 5/60. The quiet zone is 0.15 × the 20-bar simple mean of true range. The fast 3-bar slope must exceed 0.02 × mean range per bar and the structural 5-bar slope must agree. Higher-timeframe direction uses EMA21/55 and a normalized EMA21 slope. Wilder-style ADX/DI use 14-bar seeded smoothing, reset on invalid data. The research package documents the complete formulas and initialization.
These are correlated readings of price, not independent votes or economic causes. No volume, spread or scheduled-news filter is included. Sessions, futures rolls, revised data and loaded history affect readings. Flow carries history across sessions and does not close positions at session end. The lines and quiet zone are not protective stops.
Method references: Kaufman adaptive average, directional movement, and confirmed higher-timeframe requests.
Flow can stand alone as your trend filter. Add session VWAP for location or Market Anatomy for more context. When using Bearing or Atlas, turn off their candle coloring if Flow owns the colors. The scripts do not communicate or change one another’s signals, stops or sizing. Turn off Flow’s panel if another study occupies the bottom-right corner.
Research release 0.2.0. 30 offline reference and timing tests pass. Native TradingView compilation, chart behavior and alert delivery remain unverified. No trading edge has been measured; Strong is a rule-based description, not a prediction.
Download Flow 0.2 Pine v6Research package (.zip)
//@version=6
// KONATUS Flow 0.2.0 — Kutty Stauder. 5-minute multitrend research edition.
// Native TradingView compilation, chart execution and alert delivery unverified.
// A descriptive filter, not an order, protective stop or profitability claim.
indicator("KONATUS Flow · 5m Multitrend 0.2.0", shorttitle="Flow 5m", overlay=true, max_bars_back=500, dynamic_requests=true)
string quality = input.string("Balanced", "Filtering", options=["Balanced", "Strict"], tooltip="5-minute charts only. Both presets require closed 15m and 1h agreement. Strict raises strength/efficiency thresholds and reduces allowed extension. Settings are not performance-optimized.")
int confirmation = input.int(2, "Confirmation closes", minval=1, maxval=5)
bool paintBars = input.bool(true, "Color closed candles", group="Display")
bool showStructure = input.bool(true, "Show structural line and ribbon", group="Display")
bool showZone = input.bool(false, "Show quiet zone", group="Display")
bool showMarks = input.bool(true, "Show new strong trends", group="Display")
bool showPanel = input.bool(true, "Show explanation panel", group="Display")
color strongUp = input.color(#21CE91, "Strong up", group="Candle colors")
color weakUp = input.color(#689C91, "Weak up", group="Candle colors")
color strongDown = input.color(#F05F6D, "Strong down", group="Candle colors")
color weakDown = input.color(#AE858D, "Weak down", group="Candle colors")
color conflictColor = input.color(#E9B85D, "Timeframe conflict", group="Candle colors")
color pauseColor = input.color(#AE91E8, "Volatility pause", group="Candle colors")
color neutralColor = input.color(#939DAA, "Neutral", group="Candle colors")
f_valid() =>
not na(open) and not na(high) and not na(low) and not na(close) and high >= math.max(open, close) and low <= math.min(open, close) and high >= low
f_range(bool valid, int count) =>
valid ? (count > 1 ? math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) : high - low) : na
f_kama(int length, float fastLength, float slowLength, bool valid, int count) =>
float step = valid and count > 1 ? math.abs(close - close[1]) : 0.0
float path = math.sum(step, length)
float er = count > length and path > 0 ? math.max(0.0, math.min(1.0, math.abs(close - close[length]) / path)) : 0.0
float alpha = math.pow(er * (2.0 / (fastLength + 1.0) - 2.0 / (slowLength + 1.0)) + 2.0 / (slowLength + 1.0), 2)
var float average = na
average := not valid ? na : na(average[1]) ? close : average[1] + alpha * (close - average[1])
[average, er]
// Seed and reset explicitly so missing OHLC never carries an old strength estimate.
f_wilder(float value, int length, int count) =>
float total = math.sum(value, length)
var float result = na
result := count < length or na(value) ? na : count == length ? total / length : (result[1] * (length - 1) + value) / length
result
f_context() =>
bool valid = f_valid()
var int count = 0
count := valid ? count + 1 : 0
var float fast = na
var float slow = na
fast := not valid ? na : na(fast[1]) ? close : fast[1] + (2.0 / 22.0) * (close - fast[1])
slow := not valid ? na : na(slow[1]) ? close : slow[1] + (2.0 / 56.0) * (close - slow[1])
float scale = ta.sma(f_range(valid, count), 14)
float slope = (fast - fast[3]) / 3.0
bool ready = count >= 90 and not na(scale) and not na(slope)
int direction = not ready or scale <= 0 ? 0 : fast > slow and close > fast and slope > 0.02 * scale ? 1 : fast < slow and close < fast and slope < -0.02 * scale ? -1 : 0
// Every field is offset: lookahead_on must NEVER expose an unfinished HTF bar.
[direction[1], ready[1], time_close[1]]
f_direction(int direction) =>
direction == 1 ? "UP" : direction == -1 ? "DOWN" : "NEUTRAL"
f_state(int state) =>
switch state
2 => "STRONG UP"
1 => "WEAK UP"
-2 => "STRONG DOWN"
-1 => "WEAK DOWN"
3 => "TIMEFRAME CONFLICT"
4 => "VOLATILITY PAUSE"
=> "NEUTRAL"
f_color(int state) =>
switch state
2 => strongUp
1 => weakUp
-2 => strongDown
-1 => weakDown
3 => conflictColor
4 => pauseColor
=> neutralColor
bool supported = chart.is_standard and timeframe.isminutes and timeframe.multiplier == 5
int direction15 = 0
int direction60 = 0
bool ready15 = false
bool ready60 = false
int closed15 = na
int closed60 = na
// Unsupported charts do not issue accidental lower-timeframe requests.
if supported
[d15, r15, t15] = request.security(syminfo.tickerid, "15", f_context(), gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_on)
[d60, r60, t60] = request.security(syminfo.tickerid, "60", f_context(), gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_on)
direction15 := d15
direction60 := d60
ready15 := r15
ready60 := r60
closed15 := t15
closed60 := t60
bool valid = f_valid()
var int count = 0
count := valid ? count + 1 : 0
[adaptive, efficiency] = f_kama(10, 2.0, 30.0, valid, count)
[structure, _] = f_kama(30, 5.0, 60.0, valid, count)
float trueRange = f_range(valid, count)
float scale = ta.sma(trueRange, 20)
float fastSlope = (adaptive - adaptive[3]) / 3.0
float slowSlope = (structure - structure[5]) / 5.0
float halfZone = 0.15 * scale
float upward = count > 1 ? high - high[1] : 0.0
float downward = count > 1 ? low[1] - low : 0.0
float plusDM = valid ? (upward > downward and upward > 0 ? upward : 0.0) : na
float minusDM = valid ? (downward > upward and downward > 0 ? downward : 0.0) : na
float smoothTR = f_wilder(trueRange, 14, count)
float smoothPlus = f_wilder(plusDM, 14, count)
float smoothMinus = f_wilder(minusDM, 14, count)
float plusDI = smoothTR > 0 ? 100.0 * smoothPlus / smoothTR : 0.0
float minusDI = smoothTR > 0 ? 100.0 * smoothMinus / smoothTR : 0.0
float dx = plusDI + minusDI > 0 ? 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI) : 0.0
float adx = f_wilder(count >= 14 ? dx : na, 14, math.max(0, count - 13))
bool localReady = count >= 120 and not na(adx) and not na(scale) and not na(slowSlope)
int localDirection = not localReady or scale <= 0 ? 0 : adaptive > structure and close > adaptive + halfZone and fastSlope > 0.02 * scale and slowSlope > 0 ? 1 : adaptive < structure and close < adaptive - halfZone and fastSlope < -0.02 * scale and slowSlope < 0 ? -1 : 0
bool fresh15 = not na(closed15) and closed15 <= time and time - closed15 <= 30 * 60 * 1000
bool fresh60 = not na(closed60) and closed60 <= time and time - closed60 <= 120 * 60 * 1000
bool ready = supported and localReady and ready15 and ready60 and fresh15 and fresh60
bool shock = valid and count > 20 and scale[1] > 0 and trueRange > 3.0 * scale[1]
var int cooldown = 0
cooldown := not valid ? 0 : shock ? 3 : math.max(0, cooldown - 1)
bool paused = cooldown > 0
bool strict = quality == "Strict"
float minimumER = strict ? 0.40 : 0.30
float minimumADX = strict ? 25.0 : 20.0
float minimumSpread = strict ? 8.0 : 5.0
float maxExtension = strict ? 1.0 : 1.5
float extension = scale > 0 ? math.abs(close - adaptive) / scale : 0.0
bool opposing = localDirection != 0 and (direction15 == -localDirection or direction60 == -localDirection)
bool aligned = localDirection != 0 and direction15 == localDirection and direction60 == localDirection
bool efficient = efficiency >= minimumER
bool strength = adx >= minimumADX and localDirection * (plusDI - minusDI) >= minimumSpread
bool stretched = extension > maxExtension
int qualified = ready and not paused and aligned and efficient and strength and not stretched ? localDirection : 0
var int run = 0
run := qualified == 0 ? 0 : qualified == qualified[1] ? run + 1 : 1
int nextState = not ready ? 0 : paused ? 4 : localDirection == 0 ? 0 : opposing ? 3 : qualified != 0 and run >= confirmation ? 2 * localDirection : localDirection
string nextReason = not supported ? "Use standard 5-minute candles" : not valid ? "Invalid OHLC: rebuilding history" : not localReady ? "Warming up: 120 valid 5m bars" : not ready15 or not ready60 ? "Warming up: 90 bars on 15m and 1h" : not fresh15 or not fresh60 ? "Waiting for fresh closed higher-timeframe bars" : paused ? "Range shock: " + str.tostring(cooldown) + " pause bars including this close" : localDirection == 0 ? "Local structure is mixed or inside quiet zone" : opposing ? "Higher timeframe opposes the 5m direction" : not aligned ? "Higher timeframe neutral: strong withheld" : not efficient ? "Price path too inefficient" : not strength ? "ADX / directional spread below threshold" : stretched ? "Too far from adaptive line" : run < confirmation ? "Confirming: " + str.tostring(run) + "/" + str.tostring(confirmation) + " closes" : "All filters aligned"
// Publish one snapshot at each close. The open candle is intentionally not painted.
var int state = 0
var string reason = "Waiting for first closed bar"
var string snapshotDirections = "5m — / 15m — / 1h —"
var string snapshotStrength = "ADX — / ER —"
var string snapshotFilter = "Waiting for data"
if barstate.isconfirmed
state := nextState
reason := nextReason
snapshotDirections := "5m " + f_direction(localDirection) + " / 15m " + (ready15 and fresh15 ? f_direction(direction15) : "WAIT") + " / 1h " + (ready60 and fresh60 ? f_direction(direction60) : "WAIT")
snapshotStrength := "ADX " + (localReady ? str.tostring(adx, "#.0") : "—") + " / ER " + (localReady ? str.tostring(efficiency, "#.00") : "—")
snapshotFilter := quality + " · " + str.tostring(confirmation) + " closes · last closed bar"
bool eligibleEvent = barstate.isconfirmed and ready and ready[1]
bool strongUpEvent = eligibleEvent and state == 2 and state[1] != 2
bool strongDownEvent = eligibleEvent and state == -2 and state[1] != -2
bool lostEvent = eligibleEvent and math.abs(state[1]) == 2 and math.abs(state) != 2
bool conflictEvent = eligibleEvent and state == 3 and state[1] != 3
bool pauseEvent = eligibleEvent and state == 4 and state[1] != 4
bool changeEvent = eligibleEvent and state != state[1]
bool shownLocalReady = supported and (barstate.isconfirmed ? localReady : localReady[1])
bool shownReady = supported and (barstate.isconfirmed ? ready : ready[1])
float shownAdaptive = shownLocalReady ? (barstate.isconfirmed ? adaptive : adaptive[1]) : na
float shownStructure = shownLocalReady ? (barstate.isconfirmed ? structure : structure[1]) : na
float shownZone = barstate.isconfirmed ? halfZone : halfZone[1]
color stateColor = f_color(state)
fastPlot = plot(shownAdaptive, "Adaptive trend", color=stateColor, linewidth=3, style=plot.style_linebr)
slowPlot = plot(showStructure ? shownStructure : na, "Structural trend", color=color.new(#D9BD7F, 15), linewidth=1, style=plot.style_linebr)
fill(fastPlot, slowPlot, color=showStructure ? color.new(stateColor, 93) : na, title="Trend structure ribbon", fillgaps=false)
zoneUpper = plot(showZone ? shownAdaptive + shownZone : na, "Quiet zone upper", color=color.new(neutralColor, 80), style=plot.style_linebr)
zoneLower = plot(showZone ? shownAdaptive - shownZone : na, "Quiet zone lower", color=color.new(neutralColor, 80), style=plot.style_linebr)
fill(zoneUpper, zoneLower, color=color.new(neutralColor, 94), title="Quiet zone", fillgaps=false)
barcolor(paintBars and ready and barstate.isconfirmed ? stateColor : na, title="Confirmed trend strength")
plotshape(showMarks and strongUpEvent, title="New strong up", style=shape.triangleup, location=location.belowbar, color=strongUp, text="S+", textcolor=strongUp, size=size.tiny)
plotshape(showMarks and strongDownEvent, title="New strong down", style=shape.triangledown, location=location.abovebar, color=strongDown, text="S-", textcolor=strongDown, size=size.tiny)
var table panel = table.new(position.bottom_right, 1, 6, bgcolor=#11191D, frame_color=#404D4E, frame_width=1)
if barstate.islast and showPanel
table.cell(panel, 0, 0, "KONATUS FLOW / 5m · 0.2", text_color=#CFB98A, text_size=size.small)
table.cell(panel, 0, 1, shownReady ? f_state(state) : "WAIT / DATA", text_color=stateColor, text_size=size.large)
table.cell(panel, 0, 2, reason, text_color=#D5DEDF, text_size=size.small)
table.cell(panel, 0, 3, snapshotDirections, text_color=#D5DEDF, text_size=size.small)
table.cell(panel, 0, 4, snapshotStrength, text_color=#B4BEC0, text_size=size.small)
table.cell(panel, 0, 5, snapshotFilter, text_color=#B4BEC0, text_size=size.tiny)
alertcondition(strongUpEvent, "Flow: strong up", "Flow 0.2: new confirmed STRONG UP on {{ticker}} / 5m. Descriptive state, not an order.")
alertcondition(strongDownEvent, "Flow: strong down", "Flow 0.2: new confirmed STRONG DOWN on {{ticker}} / 5m. Descriptive state, not an order.")
alertcondition(lostEvent, "Flow: strong trend lost", "Flow 0.2: strong conditions no longer hold on {{ticker}} / 5m. Review the panel; not an exit order.")
alertcondition(conflictEvent, "Flow: timeframe conflict", "Flow 0.2: 5m direction opposed by a closed higher-timeframe reading on {{ticker}}.")
alertcondition(pauseEvent, "Flow: volatility pause", "Flow 0.2: confirmed volatility pause on {{ticker}} / 5m.")
alertcondition(changeEvent, "Flow: any state change", "Flow 0.2: confirmed state change on {{ticker}} / 5m. Check the explanation panel.")
Prefer the original simpler version? Download the Flow 0.1 archive.
05 / Macro price trends · Research v0.1
Know the direction.
Understand the condition.
A daily and weekly view of the market you trade. Horizon separates a lasting directional bias from a temporary pullback, a stretched move or a genuine breakdown.
Get Horizon for TradingView ↓
Daily pressure, weekly evidence and range position established an upward regime. Its holding rules still pass.
Evidence is conflicted, a new direction is still confirming, the old bias broke, or a shock calls for a pause.
The same rules work in reverse. A short bias needs its own agreement and confirmation.
Illustrative reading · Not live market data
LONG BIASPULLBACK
The last five days moved against the long bias, but the macro holding rules remain intact. That describes a condition to investigate—not a promise that the dip will recover.
A single red candle should not have to mean a new macro short. Horizon uses separate rules for establishing and retaining a bias, then tells you what kind of move is happening inside it.
The bias holds and the recent move agrees.
The five-day move opposes the bias; the holding rules still pass.
Price is more than three mean ranges beyond its recent median in the direction of the bias. Direction is not an entry price.
Daily and weekly evidence disagree, confirmation is incomplete, or the previous bias has broken.
A daily range shock clears the bias for that observation and the next two. Fresh confirmation is required afterward.
History, freshness or chart requirements are not met. Missing information is kept separate from a neutral reading.
Every entry gate must agree. The pane’s −100 to +100 evidence index summarizes these measurements; it is not a probability or a standalone trade trigger. The inputs are correlated readings of price, not independent economic evidence.
Balanced requires two qualifying daily observations. Patient raises the thresholds and requires three. Both use the same holding rules. If a holding rule fails, the bias clears to neutral; a reversal must build a fresh confirmation sequence on later days.
Horizon publishes the previous completed daily assessment in the next daily interval, including on a daily chart. Weekly evidence is what was already confirmed at that assessment’s daily open. For a normal Monday–Friday market, Friday’s new weekly bar enters Monday’s calculation and is displayed Tuesday. This deliberate delay prevents developing weekly data from being treated as confirmed.
The regime engine and confirmation counters run inside the daily context. Moving to a five-minute chart does not speed them up. Alerts fire at the first chart-bar close carrying a new snapshot; on a daily chart that means the close, although the snapshot is visible from its open. Initial availability does not trigger a bias-change alert.
With signed efficiency E, daily pressure D = .20 E21 + .35 E63 + .45 E126; weekly backbone W = .50 E13 + .50 E26. Range structure S = 2 × (close − prior low)/(prior high − low) − 1, clipped to [−1,+1]. Index = 100 × (.45 D + .35 W + .20 S). Flat denominators produce zero.
Balanced entry thresholds for directional D/W/S are .20/.15/.20; Patient uses .30/.25/.30. Holding thresholds are D > −.05, W > 0 and S > −.20, after multiplying by the bias direction. A daily true range above three times the prior 20-day simple mean range triggers the shock pause. Stretch compares the close to the prior 21-day median, using the current 20-day mean range. These defaults are unoptimized research choices.
Daily snapshots older than seven calendar days and weekly context older than 21 days are blocked. A daily gap over seven days restarts the daily warmup. Invalid data resets history. Revised feeds, adjustments, futures rolls, sessions and loaded history can change results. Slow trend rules lag, can hold through losses and can miss fast reversals.
“Macro” here means the charted asset’s larger price trend. There are no inflation, interest-rate, news, volume, liquidity or execution-cost inputs. Research on diversified time-series momentum does not validate this specific model. The source follows TradingView’s confirmed higher-timeframe request patterns.
Use Horizon to frame the broader directional case, then Flow to inspect five-minute conditions. A local pullback can coexist with an intact macro trend. The scripts do not communicate, submit orders or override each other’s readings. Horizon leaves price candles untouched.
Research release 0.1.0. 28 offline reference tests pass. Native TradingView compilation, chart behavior and alert delivery remain unverified. No trading edge has been measured.
Download Horizon Pine v6Research package (.zip)
//@version=6
// KONATUS Horizon 0.1.0 — Kutty Stauder. Macro price-trend research edition.
// Native TradingView compilation, chart behavior and alerts remain unverified.
// Bias is descriptive, not an entry, exit, protective stop or profit forecast.
indicator("KONATUS Horizon · Macro Regime 0.1.0", shorttitle="Horizon", overlay=false, max_bars_back=600, dynamic_requests=true)
string mode = input.string("Balanced", "Confirmation", options=["Balanced", "Patient"], tooltip="Balanced: 2 daily observations; Patient: 3 and higher entry thresholds. Both use identical holding rules. Neither preset is performance-optimized.")
bool showComponents = input.bool(false, "Show the three components", group="Display")
bool showBackground = input.bool(true, "Shade the active bias", group="Display")
bool showMarks = input.bool(true, "Mark bias changes", group="Display")
bool showPanel = input.bool(true, "Show explanation panel", group="Display")
color longColor = input.color(#36C9A0, "Long bias", group="Colors")
color shortColor = input.color(#F0717C, "Short bias", group="Colors")
color waitColor = input.color(#C3AC78, "Neutral / transition", group="Colors")
f_valid() =>
not na(open) and not na(high) and not na(low) and not na(close) and high >= math.max(open, close) and low <= math.min(open, close) and high >= low
f_efficiency(int length, bool valid, int count) =>
float step = valid and count > 1 ? math.abs(close - close[1]) : 0.0
float path = math.sum(step, length)
count > length and path > 0 ? math.max(-1.0, math.min(1.0, (close - close[length]) / path)) : 0.0
f_weekly() =>
bool valid = f_valid()
var int count = 0
count := valid ? count + 1 : 0
float backbone = 0.5 * f_efficiency(13, valid, count) + 0.5 * f_efficiency(26, valid, count)
bool ready = count >= 27
// Every requested field is shifted; no developing weekly vote.
[backbone[1], ready[1], time_close[1]]
f_daily() =>
// Executed inside the DAILY request, so counters cannot accelerate on 5m.
// Weekly evidence here is what was confirmed at this daily bar's OPEN.
[weekly, weeklyReady, weeklyClosed] = request.security(syminfo.tickerid, "1W", f_weekly(), gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_on)
bool valid = f_valid()
bool longGap = not na(time_close[1]) and time - time_close[1] > 7 * 86400000
var int count = 0
count := not valid ? 0 : longGap ? 1 : count + 1
float tr = valid ? (count > 1 ? math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) : high - low) : na
float scale = ta.sma(tr, 20)
float pressure = 0.20 * f_efficiency(21, valid, count) + 0.35 * f_efficiency(63, valid, count) + 0.45 * f_efficiency(126, valid, count)
float priorHigh = ta.highest(high[1], 63)
float priorLow = ta.lowest(low[1], 63)
float structure = priorHigh > priorLow ? math.max(-1.0, math.min(1.0, 2.0 * (close - priorLow) / (priorHigh - priorLow) - 1.0)) : 0.0
float balance = ta.median(close[1], 21)
float extension = scale > 0 ? (close - balance) / scale : 0.0
float impulse = close - close[5]
bool freshWeek = not na(weeklyClosed) and time >= weeklyClosed and time - weeklyClosed <= 21 * 86400000
bool ready = count >= 127 and weeklyReady and freshWeek and not na(scale) and not na(balance) and not na(priorHigh) and not na(priorLow)
bool shock = valid and count >= 21 and not na(scale[1]) and tr > 3.0 * scale[1]
float td = mode == "Patient" ? 0.30 : 0.20
float tw = mode == "Patient" ? 0.25 : 0.15
float ts = mode == "Patient" ? 0.30 : 0.20
int required = mode == "Patient" ? 3 : 2
var int bias = 0
var int candidate = 0
var int run = 0
var int pause = 0
bool invalidated = false
if not ready
bias := 0
candidate := 0
run := 0
pause := 0
else
pause := shock ? 3 : math.max(0, pause - 1)
if pause > 0
bias := 0
candidate := 0
run := 0
else
invalidated := bias != 0 and (bias * pressure <= -0.05 or bias * weekly <= 0 or bias * structure <= -0.20)
if invalidated
bias := 0
candidate := 0
run := 0
else if bias == 0
int next = pressure >= td and weekly >= tw and structure >= ts ? 1 : pressure <= -td and weekly <= -tw and structure <= -ts ? -1 : 0
run := next != 0 ? (next == candidate ? run + 1 : 1) : 0
candidate := next
if run >= required
bias := candidate
candidate := 0
run := 0
// 0 transition, 1 trend, 2 pullback, 3 stretched, 4 conflict, 5 pause, 6 unavailable.
int condition = not ready ? 6 : pause > 0 ? 5 : bias != 0 ? (bias * extension > 3 ? 3 : bias * impulse < 0 ? 2 : 1) : invalidated ? 0 : pressure * weekly < 0 ? 4 : 0
float score = ready ? 100.0 * (0.45 * pressure + 0.35 * weekly + 0.20 * structure) : na
// A daily result is published in the NEXT daily interval on every chart.
[bias[1], condition[1], score[1], pressure[1], weekly[1], structure[1], ready[1], time_close[1], weeklyClosed[1], run[1], invalidated[1]]
f_condition(int value) =>
switch value
1 => "TREND"
2 => "PULLBACK"
3 => "STRETCHED"
4 => "CONFLICT"
5 => "SHOCK PAUSE"
6 => "UNAVAILABLE"
=> "TRANSITION"
f_reason(int value, bool invalidated) =>
switch value
1 => "Macro hold rules intact"
2 => "5-day counter-move; macro bias still intact"
3 => "Extended from median; bias is not an entry"
4 => "Daily and weekly evidence oppose"
5 => "Range shock; wait for fresh confirmation"
6 => "Need 127 daily and 27 weekly valid bars"
=> invalidated ? "Prior bias broke; fresh confirmation required" : "Waiting for agreement and daily confirmation"
bool supported = chart.is_standard and ((timeframe.isminutes and timeframe.multiplier < 1440) or (timeframe.isdaily and timeframe.multiplier == 1))
int sourceBias = 0
int condition = 6
float score = na
float dailyPressure = na
float weeklyBackbone = na
float structure = na
bool sourceReady = false
int dailyClosed = na
int weeklyClosed = na
int confirmationRun = 0
bool invalidated = false
if supported
[b, c, s, d, w, r, ready, dc, wc, n, invalid] = request.security(syminfo.tickerid, "1D", f_daily(), gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_on)
sourceBias := b
condition := c
score := s
dailyPressure := d
weeklyBackbone := w
structure := r
sourceReady := ready
dailyClosed := dc
weeklyClosed := wc
confirmationRun := n
invalidated := invalid
bool fresh = not na(dailyClosed) and time >= dailyClosed and time - dailyClosed <= 7 * 86400000
bool available = supported and sourceReady and fresh
int bias = available ? sourceBias : 0
color biasColor = bias == 1 ? longColor : bias == -1 ? shortColor : waitColor
string title = not supported ? "USE STANDARD 1m–1D" : not available ? "UNAVAILABLE" : bias == 1 ? "LONG BIAS" : bias == -1 ? "SHORT BIAS" : "NEUTRAL"
string detail = not supported ? "Standard minute candles or exactly 1D" : not fresh and not na(dailyClosed) ? "Daily data stale; no active bias" : f_reason(available ? condition : 6, invalidated)
hline(0, "Balance", color=color.new(color.gray, 55))
hline(100, "Index ceiling", color=color.new(color.gray, 85))
hline(-100, "Index floor", color=color.new(color.gray, 85))
plot(available ? score : na, "Macro evidence index (not probability)", color=biasColor, linewidth=3, style=plot.style_stepline)
plot(showComponents and available ? 100 * dailyPressure : na, "Daily pressure", color=#5AA8D1)
plot(showComponents and available ? 100 * weeklyBackbone : na, "Weekly backbone", color=#B19AE1)
plot(showComponents and available ? 100 * structure : na, "Range structure", color=#C4A46B)
bgcolor(showBackground and available ? color.new(biasColor, bias == 0 ? 96 : 91) : na)
bool newSnapshot = not na(dailyClosed[1]) and dailyClosed != dailyClosed[1]
bool changed = available and available[1] and newSnapshot and sourceBias != sourceBias[1]
bool newLong = changed and sourceBias == 1
bool newShort = changed and sourceBias == -1
bool cleared = changed and sourceBias == 0
plotshape(showMarks and newLong, title="New long bias", style=shape.triangleup, location=location.bottom, color=longColor, size=size.tiny, text="L")
plotshape(showMarks and newShort, title="New short bias", style=shape.triangledown, location=location.top, color=shortColor, size=size.tiny, text="S")
plotshape(showMarks and cleared, title="Bias cleared", style=shape.xcross, location=location.bottom, color=waitColor, size=size.tiny)
alertcondition(newLong and barstate.isconfirmed, "New macro long bias", "KONATUS Horizon: new confirmed macro LONG bias on {{ticker}}. Review condition; this is not an entry order.")
alertcondition(newShort and barstate.isconfirmed, "New macro short bias", "KONATUS Horizon: new confirmed macro SHORT bias on {{ticker}}. Review condition; this is not an entry order.")
alertcondition(cleared and barstate.isconfirmed, "Macro bias cleared", "KONATUS Horizon: prior bias cleared to NEUTRAL on {{ticker}}.")
alertcondition(changed and barstate.isconfirmed, "Any macro bias change", "KONATUS Horizon: confirmed macro bias changed on {{ticker}}. Review the Horizon panel.")
var table panel = table.new(position.top_right, 2, 9, bgcolor=color.new(#101D29, 6), border_width=1, border_color=color.new(color.gray, 70))
if barstate.islast
if showPanel
table.cell(panel, 0, 0, "KONATUS HORIZON", text_color=color.white, text_size=size.small)
table.cell(panel, 1, 0, title, text_color=available ? biasColor : color.gray, text_size=size.small)
table.cell(panel, 0, 1, "Condition", text_color=color.silver)
table.cell(panel, 1, 1, available ? f_condition(condition) : "UNAVAILABLE", text_color=color.white)
table.cell(panel, 0, 2, "Why", text_color=color.silver)
table.cell(panel, 1, 2, detail, text_color=color.white, text_size=size.small)
table.cell(panel, 0, 3, "Daily / Weekly / Range", text_color=color.silver)
table.cell(panel, 1, 3, available ? str.tostring(100*dailyPressure, "#.0") + " / " + str.tostring(100*weeklyBackbone, "#.0") + " / " + str.tostring(100*structure, "#.0") : "—", text_color=color.white)
table.cell(panel, 0, 4, "Daily snapshot", text_color=color.silver)
table.cell(panel, 1, 4, not na(dailyClosed) ? str.format_time(dailyClosed, "yyyy-MM-dd HH:mm", syminfo.timezone) : "—", text_color=color.white, text_size=size.small)
table.cell(panel, 0, 5, "Weekly evidence closed", text_color=color.silver)
table.cell(panel, 1, 5, not na(weeklyClosed) ? str.format_time(weeklyClosed, "yyyy-MM-dd", syminfo.timezone) : "—", text_color=color.white)
table.cell(panel, 0, 6, "Confirmation", text_color=color.silver)
table.cell(panel, 1, 6, mode + (available and bias == 0 ? " · " + str.tostring(confirmationRun) + "/" + (mode == "Patient" ? "3" : "2") : ""), text_color=color.white)
table.cell(panel, 0, 7, "Timing", text_color=color.silver)
table.cell(panel, 1, 7, "Previous daily close; weekly at its open", text_color=color.silver, text_size=size.small)
table.cell(panel, 0, 8, "RESEARCH 0.1.0", text_color=waitColor, text_size=size.small)
table.cell(panel, 1, 8, "Bias ≠ entry · Native validation pending", text_color=color.silver, text_size=size.small)
else
table.clear(panel, 0, 0, 1, 8)
06 / Daily breakouts · Pine Script v6 · Research 0.1
Let the base form.
Let price prove it.
A quiet chart. A level that stays put. Follow a tightening base into a confirmed breakout—and see when its exit rules call time.

One setup. One attempt.
Inspired by Kristjan Kullamägi, known as Qullamaggie: prior momentum, a tightening base, and rising moving averages. Vela adds a frozen breakout level and explicit rules for waiting, accepting, or rejecting the next move.
A gold dot marks preparation. The base qualifies; its release level stops moving.
A teal triangle marks a close above the level with enough volume and a controlled extension.
Follow the 10/20-day ribbon. A thin coral line holds the breakout day’s low.
A coral cross marks a later risk touch or close below your chosen average.
A tightening base qualifies at the close. The release level is sealed at 91.96725 for the next five bars.
The next close reaches 92 with 1.5× prior average volume. A single BREAK appears; the initial risk reference is 91.
The campaign stays active above its initial reference and the selected moving average. No repeated entry triangles.
A later close falls below SMA20. EXIT ends this observation; it does not open a short position.
Gold: SMA10 and frozen release. Teal: SMA20. Dashed coral: initial risk reference. The examples contain no live market data.
From source to chart
This is long-only. EXIT is not a short signal. The script places no orders; a risk-touch alert arrives at the daily close and cannot replace an intraday protective stop. Gaps and slippage can exceed your intended risk.
Trend + prior advanceClose > SMA10 > SMA20 > SMA50; SMA20 is higher than five bars earlier. The 40-bar advance ending before the 10-bar base must be at least 20%.
Contraction + proximityBase span ≤ 3 MTR. Last five bars’ mean true range ≤ 0.75× the pre-base 20-bar mean. Close within 1 MTR of SMA10 or SMA20. MTR is the simple 20-day mean true range, not Wilder ATR or percentage ADR.
A sealed release levelBase high + 0.10 MTR, frozen on the arming close. Valid for the next five closes. A low beneath the base or lost trend cancels it. A new coil needs the readiness condition to turn off and then on.
First attempt onlyFirst close above the level needs ≥ 1.5× prior 20-day average volume, close in the top 35% of its range, extension from SMA10 ≤ 2 frozen MTR, and positive close-to-low distance ≤ 1.5 frozen MTR. A failed attempt is consumed.
Two ways to finishFrom the bar after BREAK: low at or below the breakout-day low, or close below the chosen SMA10/20. Initial-risk touch takes precedence. No automatic partial sales or break-even adjustment.
Missing informationMissing volume blocks setups by default; disabling that filter changes the rules. Price exits continue without volume. Invalid price/history clears the campaign and shows UNAVAILABLE. Standard daily candles only.
Open source · No account required to download
Three moving averages, a restrained ribbon, and clear event markers. Candle coloring is optional and off by default. Use the compact panel to read the state without covering the chart.
Research release. 24 offline reference tests pass, including a complete synthetic lifecycle. Independent source review completed. Native TradingView compilation, chart replay and alert delivery remain unverified. No trading edge or improvement in returns has been measured.
//@version=6
// Konatus Vela 0.1.0 | Kutty Stauder | https://kuttystauder.com/trading/#konatus-vela
// Inspired by Qullamaggie's published breakout framework; independent adaptation.
// Research indicator. No orders, position sizing or demonstrated trading edge.
// Standard daily candles. Events at close; no future data or intrabar entries.
indicator("KONATUS Vela · Breakout Trend", shorttitle="VELA", overlay=true, max_bars_back=500)
string setupGroup = "01 · Coil"
int baseLen = input.int(10, "Base length", minval=5, maxval=30, group=setupGroup)
int impulseLen = input.int(40, "Prior advance lookback", minval=20, maxval=120, group=setupGroup)
float minImpulse = input.float(20.0, "Minimum prior advance (%)", minval=0, maxval=200, step=5, group=setupGroup)
float contraction = input.float(0.75, "Range contraction ratio", minval=0.1, maxval=1, step=0.05, group=setupGroup)
int expiry = input.int(5, "Coil expires after bars", minval=1, maxval=20, group=setupGroup)
string releaseGroup = "02 · Release and ride"
bool requireVolume = input.bool(true, "Require volume expansion", group=releaseGroup)
float volMultiple = input.float(1.5, "Volume / prior 20-day average", minval=1, maxval=5, step=0.1, group=releaseGroup)
int trailLen = input.int(20, "Close-based trailing average", options=[10, 20], group=releaseGroup)
string styleGroup = "03 · Appearance"
bool showMAs = input.bool(true, "Show 10 / 20 / 50 averages", group=styleGroup)
bool showPanel = input.bool(true, "Show compact status", group=styleGroup)
bool paintBars = input.bool(false, "Color confirmed campaign candles", group=styleGroup)
color gold = input.color(#D7B777, "Coil / SMA10", group=styleGroup)
color teal = input.color(#72CCB1, "Breakout / SMA20", group=styleGroup)
color coral = input.color(#E58C7A, "Exit / initial risk", group=styleGroup)
color muted = #8D969A
// Rolling series are evaluated unconditionally. MTR uses simple, not Wilder smoothing.
bool supported = chart.is_standard and timeframe.isdaily and timeframe.multiplier == 1
bool priceOK = not na(open) and not na(high) and not na(low) and not na(close) and low > 0 and high >= math.max(open, close) and low <= math.min(open, close)
var int validRun = 0
validRun := priceOK ? validRun + 1 : 0
int warmup = math.max(55, math.max(baseLen + impulseLen + 1, baseLen + 21))
float ma10 = ta.sma(close, 10)
float ma20 = ta.sma(close, 20)
float ma50 = ta.sma(close, 50)
float trueRange = ta.tr(true)
float mtr = ta.sma(trueRange, 20)
float shortRange = ta.sma(trueRange, 5)
float oldRange = mtr[baseLen]
float top = ta.highest(high, baseLen)
float bottom = ta.lowest(low, baseLen)
float impulse = 100 * (close[baseLen] / close[baseLen + impulseLen] - 1)
float priorVolume = ta.sma(volume, 20)[1]
float volumeValid = ta.sma(not na(volume) and volume > 0 ? 1.0 : 0.0, 20)[1]
bool volumeData = not na(volume) and volume > 0 and volumeValid == 1 and priorVolume > 0
bool volumeOK = not requireVolume or (volumeData and volume >= volMultiple * priorVolume)
bool dataOK = supported and priceOK and validRun >= warmup and not na(mtr)
bool trend = close > ma10 and ma10 > ma20 and ma20 > ma50 and ma20 > ma20[5]
bool advance = impulse >= minImpulse
bool narrow = top - bottom <= 3 * mtr
bool contracted = shortRange <= contraction * oldRange
bool nearMA = math.min(math.abs(close - ma10), math.abs(close - ma20)) <= mtr
bool ready = dataOK and mtr > 0 and trend and advance and narrow and contracted and nearMA and (not requireVolume or volumeData)
string waiting = not trend ? "Wait for rising averages" : not advance ? "Need a prior advance" : not narrow ? "Base is too wide" : not contracted ? "Wait for contraction" : not nearMA ? "Price away from averages" : requireVolume and not volumeData ? "Volume unavailable" : "Wait for a fresh coil"
// 0 WAIT, 1 COIL, 2 RIDE. One attempt per fresh readiness edge.
var int state = 0
var int armedBar = na
var float level = na
var float floor = na
var float frozenRange = na
var float initialRisk = na
var bool previousReady = false
var string reason = "Load history"
var bool available = false
bool coilEvent = false
bool breakEvent = false
bool exitEvent = false
bool rejectEvent = false
if barstate.isconfirmed
available := dataOK
if not dataOK
state := 0
level := na
floor := na
frozenRange := na
initialRisk := na
previousReady := false
reason := not supported ? "Use standard 1D candles" : "Load valid price history"
else
if state == 2
float trail = trailLen == 10 ? ma10 : ma20
if low <= initialRisk or close < trail
reason := low <= initialRisk ? "Initial risk touched" : "Close below trail"
state := 0
exitEvent := true
level := na
initialRisk := na
else
reason := "Follow the moving average"
else if state == 1
int age = bar_index - armedBar
if age > expiry
state := 0
reason := "Coil expired"
else if low < floor or not trend
state := 0
reason := "Base or trend lost"
else if close > level
float location = high > low ? (close - low) / (high - low) : 0
bool quality = volumeOK and location >= 0.65 and close - ma10 <= 2 * frozenRange and close - low > 0 and close - low <= 1.5 * frozenRange
if quality
state := 2
breakEvent := true
initialRisk := low
reason := "Confirmed breakout"
else
state := 0
rejectEvent := true
reason := "Breakout filters failed"
else
reason := "Waiting for the release"
if state == 0
level := na
else if ready and not previousReady
state := 1
armedBar := bar_index
level := top + 0.10 * mtr
floor := bottom
frozenRange := mtr
initialRisk := na
coilEvent := true
reason := "Range sealed"
else
reason := waiting
previousReady := ready
// A live candle shows the previous completed averages, never a provisional event.
float confirmed10 = barstate.isconfirmed ? ma10 : ma10[1]
float confirmed20 = barstate.isconfirmed ? ma20 : ma20[1]
float confirmed50 = barstate.isconfirmed ? ma50 : ma50[1]
p10 = plot(supported and showMAs ? confirmed10 : na, "SMA10 · fast", color=color.new(gold, 15), linewidth=1)
p20 = plot(supported and showMAs ? confirmed20 : na, "SMA20 · trend", color=color.new(teal, 15), linewidth=2)
plot(supported and showMAs ? confirmed50 : na, "SMA50 · structure", color=color.new(muted, 45), linewidth=1)
fill(p10, p20, color=available and state > 0 ? color.new(state == 1 ? gold : teal, 93) : na, title="Campaign ribbon")
plot(available and (state == 1 or breakEvent) ? level : na, "Frozen release level", color=gold, linewidth=2, style=plot.style_linebr)
plot(available and state == 2 ? initialRisk : na, "Initial risk reference · not an order", color=color.new(coral, 20), linewidth=1, style=plot.style_linebr)
plotshape(coilEvent, title="New coil", style=shape.circle, location=location.belowbar, color=gold, size=size.tiny, text="C", textcolor=gold)
plotshape(breakEvent, title="Confirmed breakout", style=shape.triangleup, location=location.belowbar, color=teal, size=size.small, text="BREAK", textcolor=teal)
plotshape(exitEvent, title="Campaign exit", style=shape.xcross, location=location.abovebar, color=coral, size=size.tiny, text="EXIT", textcolor=coral)
barcolor(paintBars and barstate.isconfirmed and available ? exitEvent ? coral : state == 2 ? teal : state == 1 ? gold : na : na)
var table panel = table.new(position.bottom_right, 2, 3, bgcolor=color.new(#101D20, 7), frame_color=color.new(gold, 65), frame_width=1)
if barstate.islast
if showPanel
string label = not supported ? "1D ONLY" : not available ? "UNAVAILABLE" : breakEvent ? "BREAKOUT" : exitEvent ? "EXIT" : state == 2 ? "RIDE" : state == 1 ? "COIL" : "WAIT"
color accent = exitEvent ? coral : state == 2 ? teal : gold
table.cell(panel, 0, 0, "VELA", text_color=gold, text_size=size.small)
table.cell(panel, 1, 0, label, text_color=accent, text_size=size.small)
table.cell(panel, 0, 1, "At last close", text_color=muted, text_size=size.tiny)
table.cell(panel, 1, 1, not supported ? "Use standard 1D candles" : reason, text_color=color.white, text_size=size.tiny)
table.cell(panel, 0, 2, state == 2 ? "Initial risk" : "Release", text_color=muted, text_size=size.tiny)
table.cell(panel, 1, 2, state == 2 ? str.tostring(initialRisk, format.mintick) : state == 1 ? str.tostring(level, format.mintick) : "—", text_color=accent, text_size=size.small)
else
table.clear(panel, 0, 0, 1, 2)
alertcondition(coilEvent, "Vela · New coil", "VELA: new daily coil on {{ticker}}. Preparation, not an entry.")
alertcondition(breakEvent, "Vela · Breakout", "VELA: confirmed daily breakout on {{ticker}} at close {{close}}. Review risk before acting.")
alertcondition(exitEvent, "Vela · Exit", "VELA: initial risk touched or close below the trailing average on {{ticker}}. Daily-close observation, not an executed order.")
alertcondition(rejectEvent, "Vela · Rejected release", "VELA: first release failed quality filters on {{ticker}}. Wait for a fresh coil.")
The idea and its boundaries
Qullamaggie describes discretionary stock selection and often uses intraday opening-range entries, then partial profit-taking and moving-average exits. Vela’s daily-close confirmation, exact filters, five-bar expiry and frozen-level lifecycle are our own adaptation. They are not his complete system or a claim to his results.
Vela does not evaluate fundamentals, market breadth, news, liquidity or the visual quality of higher lows. Filters can delay good entries and still admit failed breakouts. Price history, sessions and data revisions can change readings. Not affiliated with or endorsed by Qullamaggie.
07 / Trend persistence · Pine Script v6 · Free research release
Wait for the break.
Follow the structure.
A trend rarely moves in a straight line. Continuation waits for a breakout to hold across closed candles, scores the evidence behind it, then follows the structure through the pullbacks.

Research prototype 0.1.0. 16 local reference and package tests pass. Native TradingView compilation, chart replay and market performance remain unverified. Better results or longer profitable trends than a moving-average crossover have not been established.
Three jobs, one chart
The first close beyond the previous 20-bar range freezes a breakout threshold. By default, a second consecutive close must stay beyond it. A close back inside cancels the candidate. More confirmation can reject brief breaks, but adds delay and can miss fast moves.
A 0–100 score combines directional efficiency, volatility-scaled slope, directional movement, candle location and volume participation. The default signal threshold is 60. Missing volume switches to price-only evidence; it does not invent buying or selling pressure.
A lower score warns of a pullback or weakening. It does not end the trend. Protection tightens only after a confirmed swing and renewed price progress. Two consecutive closes through that level invalidate the trend by default.
The pullback question
A moving-average crossover responds to the relationship between two smoothed prices. Continuation separates weakening evidence from a break in its structural rules. That gives a pullback room to develop while keeping an explicit invalidation level visible.
The tradeoff is real: waiting for structural failure can surrender more open profit. A slower exit is not automatically a better exit. The included comparison strategy lets you explore structure, EMA 10/30 and ATR Chandelier exits with the same signal engine.
These are close-based observations, not protective broker orders. Gaps, slippage and a move between candle closes can exceed the level shown on the chart.

Understand the numbers
A transparent, heuristic summary of current evidence. An 80 does not mean an 80% chance of success. The component weights and default threshold have not been fitted or validated as a forecasting model.
At entry, the script looks at previously resolved, sampled signals with the same direction, score band and volume mode. By default, success means touching a target 1.5 entry ATR away before touching the initial protection, within the next 20 bars.
The historical rate appears only after at least 20 resolved examples in that bucket, alongside a sample count and descriptive 95% interval. Otherwise it reads Insufficient history. The rate is frozen at entry; it is not a continuously updated forecast of the remaining trend.
Only one outcome window is tracked at a time. A candle touching both boundaries counts as a failure, as does a timeout. Unfinished observations are excluded. The historical touch-based outcome differs from the indicator’s close-confirmed exit. Market changes, different stop distances and correlated observations limit what the rate and interval can tell you.
From source to chart
Standard OHLCV only: no paid-only footprint API, external feed or paid script dependency. Exchange data, delays, history and alert limits still depend on your TradingView account. Different markets and timeframes need separate validation.
Version 0.1.0 · Complete source
//@version=6
indicator("Konatus Continuation [Research 0.1]", shorttitle="Konatus Trend", overlay=true, max_labels_count=200)
// Standard OHLCV only. Score is heuristic; historical rates are not calibrated forecasts.
int breakoutLength = input.int(20, "Breakout lookback", minval=5, maxval=200, group="Detection")
int confirmBars = input.int(2, "Consecutive breakout closes", minval=1, maxval=10, group="Detection")
int candidateLifetime = input.int(8, "Maximum candidate bars", minval=1, maxval=30, group="Detection")
float breakoutBuffer = input.float(0.10, "Breakout buffer (ATR)", minval=0, step=0.05, group="Detection")
int featureLength = input.int(14, "Evidence lookback", minval=5, maxval=100, group="Detection")
float minimumScore = input.float(60, "Minimum entry score / 100", minval=0, maxval=100, group="Detection")
int pivotBars = input.int(3, "Swing confirmation bars each side", minval=1, maxval=20, group="Structure")
int exitCloses = input.int(2, "Consecutive closes through protection", minval=1, maxval=10, group="Structure")
float structureBuffer = input.float(0.25, "Structure buffer (ATR)", minval=0, step=0.05, group="Structure")
float targetAtr = input.float(1.5, "Continuation target (entry ATR)", minval=0.25, step=0.25, group="Historical estimate")
int forecastBars = input.int(20, "Continuation horizon (bars)", minval=1, maxval=200, group="Historical estimate")
int minimumSamples = input.int(20, "Minimum resolved samples per bucket", minval=10, maxval=500, group="Historical estimate")
bool showLabels = input.bool(true, "Show entry and exit labels", group="Display")
bool shadeTrend = input.bool(true, "Shade active trend", group="Display")
clamp01(float value) =>
math.max(0.0, math.min(1.0, value))
bucketFor(int direction, float score, bool hasVolume) =>
(score < 70 ? 0 : score < 85 ? 1 : 2) + (direction == 1 ? 0 : 3) + (hasVolume ? 0 : 6)
wilson(int successes, int samples) =>
float rate = samples > 0 ? float(successes) / samples : na
float denominator = samples > 0 ? 1.0 + 3.8416 / samples : na
float center = samples > 0 ? (rate + 3.8416 / (2.0 * samples)) / denominator : na
float halfWidth = samples > 0 ? 1.96 * math.sqrt(rate * (1 - rate) / samples + 3.8416 / (4.0 * samples * samples)) / denominator : na
[rate, math.max(0.0, center - halfWidth), math.min(1.0, center + halfWidth)]
// Calculate every bar; commit decisions only when the chart bar closes.
float atr = ta.atr(featureLength)
float priorHigh = ta.highest(high, breakoutLength)[1]
float priorLow = ta.lowest(low, breakoutLength)[1]
float pivotLow = ta.pivotlow(low, pivotBars, pivotBars)
float pivotHigh = ta.pivothigh(high, pivotBars, pivotBars)
float pathLength = ta.sma(math.abs(ta.change(close)), featureLength) * featureLength
float efficiency = pathLength > 0 ? (close - close[featureLength]) / pathLength : 0
float regressionSlope = ta.linreg(close, featureLength, 0) - ta.linreg(close, featureLength, 1)
float scaledSlope = atr > 0 ? regressionSlope * featureLength / (4.0 * atr) : 0
[plusDI, minusDI, adx] = ta.dmi(featureLength, featureLength)
float directionBalance = plusDI + minusDI > 0 ? (plusDI - minusDI) / (plusDI + minusDI) : 0
float closeLocation = high > low ? (2.0 * close - high - low) / (high - low) : 0
float meanLocation = ta.sma(closeLocation, featureLength)
float meanVolume = ta.sma(volume, featureLength)
float volumeCompleteness = ta.sma(not na(volume) and volume >= 0 ? 1.0 : 0.0, featureLength)
bool hasVolume = volumeCompleteness == 1 and meanVolume > 0
float relativeVolume = hasVolume ? volume / meanVolume : 1
float signedVolumeMean = ta.sma(volume * closeLocation, featureLength)
float volumePressure = hasVolume ? signedVolumeMean / meanVolume * math.min(relativeVolume, 2.0) : 0
evidenceScore(int direction) =>
float efficiencyPart = clamp01(0.5 + 0.5 * direction * efficiency)
float slopePart = clamp01(0.5 + direction * scaledSlope)
float movementPart = clamp01(direction * directionBalance) * clamp01(adx / 35.0)
float locationPart = clamp01(0.5 + 0.5 * direction * meanLocation)
float volumePart = clamp01(0.5 + 0.5 * direction * volumePressure)
float weighted = 25 * efficiencyPart + 25 * slopePart + 20 * movementPart + 15 * locationPart
100.0 * (weighted + (hasVolume ? 15 * volumePart : 0)) / (hasVolume ? 100.0 : 85.0)
float longScore = evidenceScore(1)
float shortScore = evidenceScore(-1)
bool ready = bar_index >= math.max(breakoutLength + 1, featureLength * 3) and atr > 0 and not na(longScore) and not na(shortScore)
var int trend = 0
var float protection = na
var float favorableExtreme = na
var float pendingProtection = na
var int entryBar = na
var int breachCount = 0
var int candidate = 0
var int candidateCount = 0
var float candidateThreshold = na
var float confirmedScore = na
var bool confirmedVolume = false
var bool warmedUp = false
// Fixed-size counters: 3 score bands x 2 directions x 2 volume availability states.
var array<int> successes = array.new_int(12, 0)
var array<int> samples = array.new_int(12, 0)
var int observationDirection = 0
var int observationBar = na
var int observationBucket = na
var float observationTarget = na
var float observationStop = na
var float entryRate = na
var float entryLower = na
var float entryUpper = na
var int entrySamples = 0
var float entryScore = na
var int skippedObservations = 0
var bool entrySampled = false
bool longSignal = false
bool shortSignal = false
bool longExit = false
bool shortExit = false
if barstate.isconfirmed
warmedUp := ready
confirmedVolume := hasVolume
// Mature prior observation BEFORE taking a new forecast snapshot.
if observationDirection != 0 and bar_index > observationBar
bool stopHit = observationDirection == 1 ? low <= observationStop : high >= observationStop
bool targetHit = observationDirection == 1 ? high >= observationTarget : low <= observationTarget
bool timedOut = bar_index - observationBar >= forecastBars
if stopHit or targetHit or timedOut
int count = array.get(samples, observationBucket)
array.set(samples, observationBucket, count + 1)
// Conservative ordering for candles containing both boundaries.
if targetHit and not stopHit
array.set(successes, observationBucket, array.get(successes, observationBucket) + 1)
observationDirection := 0
if trend != 0
// Pivot origins must be after this trend's entry; no backdated signals.
float newPivot = trend == 1 ? pivotLow : pivotHigh
if not na(newPivot) and bar_index - pivotBars > entryBar
pendingProtection := newPivot - trend * structureBuffer * atr
bool broken = trend * (close - protection) < 0
breachCount := broken ? breachCount + 1 : 0
if breachCount >= exitCloses
longExit := trend == 1
shortExit := trend == -1
if showLabels
label.new(bar_index, close, "Trend invalidated", style=label.style_label_left, color=color.new(color.gray, 15), textcolor=color.white, size=size.tiny)
trend := 0
protection := na
pendingProtection := na
breachCount := 0
else
bool progress = trend * (close - favorableExtreme) > 0
if not broken and progress and not na(pendingProtection) and trend * (close - pendingProtection) > 0
protection := trend == 1 ? math.max(protection, pendingProtection) : math.min(protection, pendingProtection)
pendingProtection := na
favorableExtreme := trend == 1 ? math.max(favorableExtreme, high) : math.min(favorableExtreme, low)
// Exit bars cannot also become entry bars.
if ready and trend == 0 and not longExit and not shortExit
if candidate == 0
if close > priorHigh + breakoutBuffer * atr
candidate := 1
candidateThreshold := priorHigh + breakoutBuffer * atr
candidateCount := 0
else if close < priorLow - breakoutBuffer * atr
candidate := -1
candidateThreshold := priorLow - breakoutBuffer * atr
candidateCount := 0
if candidate != 0
bool holdsBreakout = candidate * (close - candidateThreshold) > 0
if not holdsBreakout
candidate := 0
candidateCount := 0
else
candidateCount += 1
float setupScore = candidate == 1 ? longScore : shortScore
if candidateCount >= confirmBars and setupScore >= minimumScore
trend := candidate
entryBar := bar_index
entryScore := setupScore
protection := trend == 1 ? priorLow - structureBuffer * atr : priorHigh + structureBuffer * atr
favorableExtreme := trend == 1 ? high : low
pendingProtection := na
breachCount := 0
longSignal := trend == 1
shortSignal := trend == -1
int key = bucketFor(trend, setupScore, hasVolume)
entrySamples := array.get(samples, key)
[rate, lower, upper] = wilson(array.get(successes, key), entrySamples)
entryRate := entrySamples >= minimumSamples ? rate : na
entryLower := entrySamples >= minimumSamples ? lower : na
entryUpper := entrySamples >= minimumSamples ? upper : na
entrySampled := observationDirection == 0
if entrySampled
observationDirection := trend
observationBar := bar_index
observationBucket := key
observationTarget := close + trend * targetAtr * atr
observationStop := protection
else
skippedObservations += 1
if showLabels
string probabilityText = na(entryRate) ? "History: n=" + str.tostring(entrySamples) : "Historical: " + str.tostring(entryRate * 100, "#.0") + "% (n=" + str.tostring(entrySamples) + ")"
string labelText = (trend == 1 ? "UP TREND" : "DOWN TREND") + "\nScore " + str.tostring(setupScore, "#.0") + "/100\n" + probabilityText
label.new(bar_index, trend == 1 ? low : high, labelText, style=trend == 1 ? label.style_label_up : label.style_label_down, color=trend == 1 ? color.teal : color.maroon, textcolor=color.white, size=size.small, tooltip="Historical rate uses prior resolved setups in this score band, direction and volume mode. Not a calibrated forecast. Target and horizon are in settings.")
candidate := 0
candidateCount := 0
else if candidateCount >= math.max(candidateLifetime, confirmBars)
candidate := 0
candidateCount := 0
confirmedScore := ready ? (trend == 1 ? longScore : trend == -1 ? shortScore : math.max(longScore, shortScore)) : na
// All displayed state is based on the last confirmed bar.
color trendColor = trend == 1 ? color.teal : trend == -1 ? color.maroon : color.gray
plot(trend != 0 ? protection : na, "Structural protection", color=trendColor, linewidth=2, style=plot.style_linebr)
plot(candidate != 0 ? candidateThreshold : na, "Pending breakout", color=color.orange, style=plot.style_linebr)
bgcolor(shadeTrend and trend != 0 ? color.new(trendColor, 92) : na)
plot(confirmedScore, "Continuation score", display=display.data_window)
plot(trend != 0 ? entryRate * 100 : na, "Entry historical continuation %", display=display.data_window)
plot(longSignal ? 1 : shortSignal ? -1 : 0, "Entry signal", display=display.data_window)
plot(longExit ? 1 : shortExit ? -1 : 0, "Exit signal", display=display.data_window)
alertcondition(longSignal, "Up trend confirmed", "Konatus: up trend confirmed on {{ticker}} {{interval}}. Score {{plot(\"Continuation score\")}}.")
alertcondition(shortSignal, "Down trend confirmed", "Konatus: down trend confirmed on {{ticker}} {{interval}}. Score {{plot(\"Continuation score\")}}.")
alertcondition(longExit or shortExit, "Trend invalidated", "Konatus: structural trend invalidated on {{ticker}} {{interval}}.")
var table panel = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 15), border_width=1)
if barstate.islast
string stateText = not warmedUp ? "Warming up" : trend == 0 ? (candidate == 0 ? "Waiting for breakout" : "Confirming " + str.tostring(candidateCount) + "/" + str.tostring(confirmBars)) : (trend == 1 ? "UP" : "DOWN") + (breachCount > 0 ? " / structure threatened" : confirmedScore < minimumScore ? " / pullback or weakening" : " / healthy")
string rateText = trend == 0 ? "--" : na(entryRate) ? "Insufficient history" : str.tostring(entryRate * 100, "#.0") + "%"
string intervalText = trend == 0 ? "--" : na(entryRate) ? "n=" + str.tostring(entrySamples) + " / need " + str.tostring(minimumSamples) : str.tostring(entryLower * 100, "#.0") + "–" + str.tostring(entryUpper * 100, "#.0") + "% / n=" + str.tostring(entrySamples)
table.cell(panel, 0, 0, "KONATUS CONTINUATION", text_color=color.white)
table.cell(panel, 1, 0, "Research 0.1", text_color=color.silver)
table.cell(panel, 0, 1, "Trend", text_color=color.silver)
table.cell(panel, 1, 1, stateText, text_color=breachCount > 0 ? color.orange : color.white)
table.cell(panel, 0, 2, "Score (not probability)", text_color=color.silver)
table.cell(panel, 1, 2, na(confirmedScore) ? "--" : str.tostring(confirmedScore, "#.0") + "/100", text_color=color.white)
table.cell(panel, 0, 3, "Historical rate at entry", text_color=color.silver)
table.cell(panel, 1, 3, rateText, text_color=color.white)
table.cell(panel, 0, 4, "95% interval / samples", text_color=color.silver)
table.cell(panel, 1, 4, intervalText, text_color=color.white)
table.cell(panel, 0, 5, "Continuation definition", text_color=color.silver)
table.cell(panel, 1, 5, str.tostring(targetAtr) + " ATR / " + str.tostring(forecastBars) + " bars", text_color=color.white)
table.cell(panel, 0, 6, "Volume", text_color=color.silver)
table.cell(panel, 1, 6, confirmedVolume ? "Chart feed volume" : "Unavailable / price only", text_color=color.white)
table.cell(panel, 0, 7, "Observation at entry", text_color=color.silver)
table.cell(panel, 1, 7, trend == 0 ? "--" : entrySampled ? "Tracked" : "Skipped: prior still active", text_color=color.silver)
The separate strategy selects Structure, EMA 10/30 or ATR Chandelier exits. Set realistic costs and sizing in Strategy Properties. Separate runs may take different subsets of entries when one position stays open longer; that is not a controlled comparison of identical trades.
Download the comparison strategy
//@version=6
strategy("Konatus Continuation — Exit Comparison [Research 0.1]", overlay=true, max_labels_count=200, pyramiding=0, default_qty_type=strategy.fixed, default_qty_value=1, commission_type=strategy.commission.percent, commission_value=0.01, slippage=1, process_orders_on_close=false)
// Standard OHLCV only. Score is heuristic; historical rates are not calibrated forecasts.
int breakoutLength = input.int(20, "Breakout lookback", minval=5, maxval=200, group="Detection")
int confirmBars = input.int(2, "Consecutive breakout closes", minval=1, maxval=10, group="Detection")
int candidateLifetime = input.int(8, "Maximum candidate bars", minval=1, maxval=30, group="Detection")
float breakoutBuffer = input.float(0.10, "Breakout buffer (ATR)", minval=0, step=0.05, group="Detection")
int featureLength = input.int(14, "Evidence lookback", minval=5, maxval=100, group="Detection")
float minimumScore = input.float(60, "Minimum entry score / 100", minval=0, maxval=100, group="Detection")
int pivotBars = input.int(3, "Swing confirmation bars each side", minval=1, maxval=20, group="Structure")
int exitCloses = input.int(2, "Consecutive closes through protection", minval=1, maxval=10, group="Structure")
float structureBuffer = input.float(0.25, "Structure buffer (ATR)", minval=0, step=0.05, group="Structure")
float targetAtr = input.float(1.5, "Continuation target (entry ATR)", minval=0.25, step=0.25, group="Historical estimate")
int forecastBars = input.int(20, "Continuation horizon (bars)", minval=1, maxval=200, group="Historical estimate")
int minimumSamples = input.int(20, "Minimum resolved samples per bucket", minval=10, maxval=500, group="Historical estimate")
bool showLabels = input.bool(true, "Show entry and exit labels", group="Display")
bool shadeTrend = input.bool(true, "Shade active trend", group="Display")
clamp01(float value) =>
math.max(0.0, math.min(1.0, value))
bucketFor(int direction, float score, bool hasVolume) =>
(score < 70 ? 0 : score < 85 ? 1 : 2) + (direction == 1 ? 0 : 3) + (hasVolume ? 0 : 6)
wilson(int successes, int samples) =>
float rate = samples > 0 ? float(successes) / samples : na
float denominator = samples > 0 ? 1.0 + 3.8416 / samples : na
float center = samples > 0 ? (rate + 3.8416 / (2.0 * samples)) / denominator : na
float halfWidth = samples > 0 ? 1.96 * math.sqrt(rate * (1 - rate) / samples + 3.8416 / (4.0 * samples * samples)) / denominator : na
[rate, math.max(0.0, center - halfWidth), math.min(1.0, center + halfWidth)]
// Calculate every bar; commit decisions only when the chart bar closes.
float atr = ta.atr(featureLength)
float priorHigh = ta.highest(high, breakoutLength)[1]
float priorLow = ta.lowest(low, breakoutLength)[1]
float pivotLow = ta.pivotlow(low, pivotBars, pivotBars)
float pivotHigh = ta.pivothigh(high, pivotBars, pivotBars)
float pathLength = ta.sma(math.abs(ta.change(close)), featureLength) * featureLength
float efficiency = pathLength > 0 ? (close - close[featureLength]) / pathLength : 0
float regressionSlope = ta.linreg(close, featureLength, 0) - ta.linreg(close, featureLength, 1)
float scaledSlope = atr > 0 ? regressionSlope * featureLength / (4.0 * atr) : 0
[plusDI, minusDI, adx] = ta.dmi(featureLength, featureLength)
float directionBalance = plusDI + minusDI > 0 ? (plusDI - minusDI) / (plusDI + minusDI) : 0
float closeLocation = high > low ? (2.0 * close - high - low) / (high - low) : 0
float meanLocation = ta.sma(closeLocation, featureLength)
float meanVolume = ta.sma(volume, featureLength)
float volumeCompleteness = ta.sma(not na(volume) and volume >= 0 ? 1.0 : 0.0, featureLength)
bool hasVolume = volumeCompleteness == 1 and meanVolume > 0
float relativeVolume = hasVolume ? volume / meanVolume : 1
float signedVolumeMean = ta.sma(volume * closeLocation, featureLength)
float volumePressure = hasVolume ? signedVolumeMean / meanVolume * math.min(relativeVolume, 2.0) : 0
evidenceScore(int direction) =>
float efficiencyPart = clamp01(0.5 + 0.5 * direction * efficiency)
float slopePart = clamp01(0.5 + direction * scaledSlope)
float movementPart = clamp01(direction * directionBalance) * clamp01(adx / 35.0)
float locationPart = clamp01(0.5 + 0.5 * direction * meanLocation)
float volumePart = clamp01(0.5 + 0.5 * direction * volumePressure)
float weighted = 25 * efficiencyPart + 25 * slopePart + 20 * movementPart + 15 * locationPart
100.0 * (weighted + (hasVolume ? 15 * volumePart : 0)) / (hasVolume ? 100.0 : 85.0)
float longScore = evidenceScore(1)
float shortScore = evidenceScore(-1)
bool ready = bar_index >= math.max(breakoutLength + 1, featureLength * 3) and atr > 0 and not na(longScore) and not na(shortScore)
var int trend = 0
var float protection = na
var float favorableExtreme = na
var float pendingProtection = na
var int entryBar = na
var int breachCount = 0
var int candidate = 0
var int candidateCount = 0
var float candidateThreshold = na
var float confirmedScore = na
var bool confirmedVolume = false
var bool warmedUp = false
// Fixed-size counters: 3 score bands x 2 directions x 2 volume availability states.
var array<int> successes = array.new_int(12, 0)
var array<int> samples = array.new_int(12, 0)
var int observationDirection = 0
var int observationBar = na
var int observationBucket = na
var float observationTarget = na
var float observationStop = na
var float entryRate = na
var float entryLower = na
var float entryUpper = na
var int entrySamples = 0
var float entryScore = na
var int skippedObservations = 0
var bool entrySampled = false
bool longSignal = false
bool shortSignal = false
bool longExit = false
bool shortExit = false
if barstate.isconfirmed
warmedUp := ready
confirmedVolume := hasVolume
// Mature prior observation BEFORE taking a new forecast snapshot.
if observationDirection != 0 and bar_index > observationBar
bool stopHit = observationDirection == 1 ? low <= observationStop : high >= observationStop
bool targetHit = observationDirection == 1 ? high >= observationTarget : low <= observationTarget
bool timedOut = bar_index - observationBar >= forecastBars
if stopHit or targetHit or timedOut
int count = array.get(samples, observationBucket)
array.set(samples, observationBucket, count + 1)
// Conservative ordering for candles containing both boundaries.
if targetHit and not stopHit
array.set(successes, observationBucket, array.get(successes, observationBucket) + 1)
observationDirection := 0
if trend != 0
// Pivot origins must be after this trend's entry; no backdated signals.
float newPivot = trend == 1 ? pivotLow : pivotHigh
if not na(newPivot) and bar_index - pivotBars > entryBar
pendingProtection := newPivot - trend * structureBuffer * atr
bool broken = trend * (close - protection) < 0
breachCount := broken ? breachCount + 1 : 0
if breachCount >= exitCloses
longExit := trend == 1
shortExit := trend == -1
if showLabels
label.new(bar_index, close, "Trend invalidated", style=label.style_label_left, color=color.new(color.gray, 15), textcolor=color.white, size=size.tiny)
trend := 0
protection := na
pendingProtection := na
breachCount := 0
else
bool progress = trend * (close - favorableExtreme) > 0
if not broken and progress and not na(pendingProtection) and trend * (close - pendingProtection) > 0
protection := trend == 1 ? math.max(protection, pendingProtection) : math.min(protection, pendingProtection)
pendingProtection := na
favorableExtreme := trend == 1 ? math.max(favorableExtreme, high) : math.min(favorableExtreme, low)
// Exit bars cannot also become entry bars.
if ready and trend == 0 and not longExit and not shortExit
if candidate == 0
if close > priorHigh + breakoutBuffer * atr
candidate := 1
candidateThreshold := priorHigh + breakoutBuffer * atr
candidateCount := 0
else if close < priorLow - breakoutBuffer * atr
candidate := -1
candidateThreshold := priorLow - breakoutBuffer * atr
candidateCount := 0
if candidate != 0
bool holdsBreakout = candidate * (close - candidateThreshold) > 0
if not holdsBreakout
candidate := 0
candidateCount := 0
else
candidateCount += 1
float setupScore = candidate == 1 ? longScore : shortScore
if candidateCount >= confirmBars and setupScore >= minimumScore
trend := candidate
entryBar := bar_index
entryScore := setupScore
protection := trend == 1 ? priorLow - structureBuffer * atr : priorHigh + structureBuffer * atr
favorableExtreme := trend == 1 ? high : low
pendingProtection := na
breachCount := 0
longSignal := trend == 1
shortSignal := trend == -1
int key = bucketFor(trend, setupScore, hasVolume)
entrySamples := array.get(samples, key)
[rate, lower, upper] = wilson(array.get(successes, key), entrySamples)
entryRate := entrySamples >= minimumSamples ? rate : na
entryLower := entrySamples >= minimumSamples ? lower : na
entryUpper := entrySamples >= minimumSamples ? upper : na
entrySampled := observationDirection == 0
if entrySampled
observationDirection := trend
observationBar := bar_index
observationBucket := key
observationTarget := close + trend * targetAtr * atr
observationStop := protection
else
skippedObservations += 1
if showLabels
string probabilityText = na(entryRate) ? "History: n=" + str.tostring(entrySamples) : "Historical: " + str.tostring(entryRate * 100, "#.0") + "% (n=" + str.tostring(entrySamples) + ")"
string labelText = (trend == 1 ? "UP TREND" : "DOWN TREND") + "\nScore " + str.tostring(setupScore, "#.0") + "/100\n" + probabilityText
label.new(bar_index, trend == 1 ? low : high, labelText, style=trend == 1 ? label.style_label_up : label.style_label_down, color=trend == 1 ? color.teal : color.maroon, textcolor=color.white, size=size.small, tooltip="Historical rate uses prior resolved setups in this score band, direction and volume mode. Not a calibrated forecast. Target and horizon are in settings.")
candidate := 0
candidateCount := 0
else if candidateCount >= math.max(candidateLifetime, confirmBars)
candidate := 0
candidateCount := 0
confirmedScore := ready ? (trend == 1 ? longScore : trend == -1 ? shortScore : math.max(longScore, shortScore)) : na
// All displayed state is based on the last confirmed bar.
color trendColor = trend == 1 ? color.teal : trend == -1 ? color.maroon : color.gray
plot(trend != 0 ? protection : na, "Structural protection", color=trendColor, linewidth=2, style=plot.style_linebr)
plot(candidate != 0 ? candidateThreshold : na, "Pending breakout", color=color.orange, style=plot.style_linebr)
bgcolor(shadeTrend and trend != 0 ? color.new(trendColor, 92) : na)
plot(confirmedScore, "Continuation score", display=display.data_window)
plot(trend != 0 ? entryRate * 100 : na, "Entry historical continuation %", display=display.data_window)
plot(longSignal ? 1 : shortSignal ? -1 : 0, "Entry signal", display=display.data_window)
plot(longExit ? 1 : shortExit ? -1 : 0, "Exit signal", display=display.data_window)
var table panel = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 15), border_width=1)
if barstate.islast
string stateText = not warmedUp ? "Warming up" : trend == 0 ? (candidate == 0 ? "Waiting for breakout" : "Confirming " + str.tostring(candidateCount) + "/" + str.tostring(confirmBars)) : (trend == 1 ? "UP" : "DOWN") + (breachCount > 0 ? " / structure threatened" : confirmedScore < minimumScore ? " / pullback or weakening" : " / healthy")
string rateText = trend == 0 ? "--" : na(entryRate) ? "Insufficient history" : str.tostring(entryRate * 100, "#.0") + "%"
string intervalText = trend == 0 ? "--" : na(entryRate) ? "n=" + str.tostring(entrySamples) + " / need " + str.tostring(minimumSamples) : str.tostring(entryLower * 100, "#.0") + "–" + str.tostring(entryUpper * 100, "#.0") + "% / n=" + str.tostring(entrySamples)
table.cell(panel, 0, 0, "KONATUS CONTINUATION", text_color=color.white)
table.cell(panel, 1, 0, "Research 0.1", text_color=color.silver)
table.cell(panel, 0, 1, "Trend", text_color=color.silver)
table.cell(panel, 1, 1, stateText, text_color=breachCount > 0 ? color.orange : color.white)
table.cell(panel, 0, 2, "Score (not probability)", text_color=color.silver)
table.cell(panel, 1, 2, na(confirmedScore) ? "--" : str.tostring(confirmedScore, "#.0") + "/100", text_color=color.white)
table.cell(panel, 0, 3, "Historical rate at entry", text_color=color.silver)
table.cell(panel, 1, 3, rateText, text_color=color.white)
table.cell(panel, 0, 4, "95% interval / samples", text_color=color.silver)
table.cell(panel, 1, 4, intervalText, text_color=color.white)
table.cell(panel, 0, 5, "Continuation definition", text_color=color.silver)
table.cell(panel, 1, 5, str.tostring(targetAtr) + " ATR / " + str.tostring(forecastBars) + " bars", text_color=color.white)
table.cell(panel, 0, 6, "Volume", text_color=color.silver)
table.cell(panel, 1, 6, confirmedVolume ? "Chart feed volume" : "Unavailable / price only", text_color=color.white)
table.cell(panel, 0, 7, "Observation at entry", text_color=color.silver)
table.cell(panel, 1, 7, trend == 0 ? "--" : entrySampled ? "Tracked" : "Skipped: prior still active", text_color=color.silver)
// Benchmark harness. Signals occur at close; market orders fill next bar by default.
string exitMode = input.string("Structure", "Exit method", options=["Structure", "EMA 10/30", "ATR Chandelier"], group="Comparison")
float trailMultiple = input.float(3.0, "Chandelier ATR distance", minval=0.5, step=0.25, group="Comparison")
int testStart = input.time(timestamp("01 Jan 2020 00:00 +0000"), "First signal date", group="Comparison")
int testEnd = input.time(timestamp("01 Jan 2035 00:00 +0000"), "Last signal date", group="Comparison")
float fastEma = ta.ema(close, 10)
float slowEma = ta.ema(close, 30)
var float tradeExtreme = na
var float chandelier = na
bool tradeOpened = strategy.position_size != 0 and strategy.position_size[1] == 0
if barstate.isconfirmed
if tradeOpened
tradeExtreme := strategy.position_size > 0 ? high : low
chandelier := strategy.position_size > 0 ? tradeExtreme - trailMultiple * atr : tradeExtreme + trailMultiple * atr
if strategy.position_size != 0
bool isLong = strategy.position_size > 0
bool structureExit = isLong ? longExit : shortExit
bool emaExit = isLong ? fastEma < slowEma : fastEma > slowEma
// Test against the trail known BEFORE this candle, then ratchet for next bar.
bool atrExit = not na(chandelier) and (isLong ? close < chandelier : close > chandelier)
bool chosenExit = exitMode == "Structure" ? structureExit : exitMode == "EMA 10/30" ? emaExit : atrExit
if chosenExit or time >= testEnd
strategy.close_all(comment=time >= testEnd ? "End of window" : exitMode)
tradeExtreme := isLong ? math.max(tradeExtreme, high) : math.min(tradeExtreme, low)
float proposedTrail = isLong ? tradeExtreme - trailMultiple * atr : tradeExtreme + trailMultiple * atr
chandelier := isLong ? math.max(chandelier, proposedTrail) : math.min(chandelier, proposedTrail)
else
tradeExtreme := na
chandelier := na
if time >= testStart and time < testEnd
if longSignal
strategy.entry("Long", strategy.long)
if shortSignal
strategy.entry("Short", strategy.short)
plot(exitMode == "ATR Chandelier" and strategy.position_size != 0 ? chandelier : na, "Comparison ATR trail", color=color.orange, style=plot.style_linebr)
// alertcondition() is omitted from this strategy; use strategy order-fill alerts.
Evaluate net results, drawdown and profit giveback on unseen periods. Longer holding time alone is not evidence of improvement. Read the trading and research disclaimer ↓
08 / Adaptive trend ribbon · Pine Script v6 · Research release
240 moving averages.
One picture of trend structure.
Spectrum blends four moving-average families into 20 colorful strands. Watch agreement spread across shorter and longer horizons, then use momentum and price efficiency to distinguish an orderly trend from a tangled market.

Research prototype · updated source. All 35 local reference and source tests pass. The current source still needs native TradingView compilation and chart validation; the earlier version passed those checks. Replay, alert delivery and broad runtime profiling remain unverified. No advantage over a simple moving-average ribbon has been established.
Adaptive at the core
The default adaptive share is 65%, adjustable in settings. Each family runs across 60 distinct lengths: 240 moving-average outputs. Three neighboring lengths, each blending all four families, form one visible strand.
Longer adaptive lookbacks do not always respond more slowly. Spectrum measures actual slopes and ordering; it never sorts lines by price to manufacture a neat ribbon. These averages share the same price history and are not independent confirmations.
Read the picture
The live ribbon can change during an open candle. Dashboard states and alerts update only on closed candles, so they can temporarily differ from the ribbon.
Structure plus momentum
ROC windows of 8, 21 and 55 bars compare immediate momentum with the broader move. A volatility-scaled blend joins slope votes, adjacent-strand ordering and 21-bar price efficiency. Scores describe structure; they are not probabilities.
By default, two consecutive closed candles must meet the trend criteria. A lower exit threshold reduces rapid direction changes. The dashboard also shows slope balance, normalized ROC, ribbon width and efficiency; the guide in the download explains every threshold.
A retained BULL or BEAR bias does not mean the trend is still healthy. The Trend health row reads Strong when all entry evidence supports that direction and the ribbon is not compressed. Slow support below 35% makes it Weakening; otherwise compression or efficiency below 20% makes it Stalled. Other failures of entry evidence make it Weakening. Without an established direction, health reads Unestablished. Fracturing and Pullback take priority in the main state row, with health still visible beside them.
The Structure row reads Expanding, Contracting or Steady. A Compressed prefix can appear with any of them, including Compressed · Expanding: the ribbon can be narrow and already starting to widen. Motion uses a five-bar EMA of ATR-normalized width and its average change over three bars; changes within ±0.01 width units per bar are Steady. Because the measure is relative to ATR, changing volatility can affect it too.
Filtered recovery
A tiny momentum crossing around zero no longer creates a pullback on its own. A pullback starts when signed fast slope balance falls below −10%, or when the eight-bar price change, divided by ATR × √8 and signed for the retained direction, falls below the −0.10 default deadband.
Recovery requires two consecutive supportive closes by default: signed fast slope balance at least 10%, signed momentum above +0.10, Strong trend health and no compression. The dashboard shows progress such as Pullback (1/2). A non-supportive close resets the count. Fracturing, invalidation, reversal or warmup cancels the pending recovery.
Both the momentum deadband and number of supportive closes are adjustable. The stricter defaults add confirmation delay. A Stalled or Weakening trend becoming Strong does not emit a recovery alert unless an actual pullback was observed and recovery confirmed. The ribbon’s 20 strands and colors are unchanged.
Start on a standard candle chart
Designed for initial exploration on 5–60 minute charts. Lookbacks count chart bars, not minutes. Start with Balanced and 65% adaptive weight.
The ROC windows remain 8/21/55 in every preset. Warmup reduces initialization effects but does not guarantee convergence. The first eligible bar emits no alert. Loaded history, data revisions and input changes can affect historical readings.
Upgrading an existing installation: replace the complete Pine source, start with 2 supportive closes and a 0.10 momentum deadband, and recreate existing alerts. This version changes pullback/recovery timing; bull/bear direction keeps its earlier confirmation and invalidation rules.
Recovery alerts require a confirmed transition from Pullback to Trending in the same direction. Alerts mark state changes rather than repeating on every candle. The ribbon has no BULL, BEAR or RESUME chart labels; the optional dashboard and all four alerts remain available. No volume feed, imported library or secondary symbol is required.
Complete Pine Script v6 source
//@version=6
// Konatus Spectrum | kuttystauder.com
// Research version: local reference tests are not TradingView compilation.
// 240 MA outputs: KAMA + FRAMA + HMA + EMA across 60 independent lookbacks.
indicator("Konatus Spectrum | Adaptive Ribbon", shorttitle="KS Spectrum", overlay=true, max_bars_back=1500)
string engineGroup = "Adaptive engine"
string preset = input.string("Balanced", "Intraday sensitivity", options=["Fast", "Balanced", "Patient"], group=engineGroup)
float adaptiveShare = input.float(0.65, "Adaptive weight", minval=0.10, maxval=0.90, step=0.05, group=engineGroup, tooltip="Split between KAMA and FRAMA. Remaining weight is split between Hull and EMA. Higher is not necessarily better.")
int slopeBars = input.int(3, "Slope lookback", minval=1, maxval=10, group=engineGroup)
float slopeFloor = input.float(0.01, "Neutral slope (ATR per bar)", minval=0.0, maxval=0.10, step=0.005, group=engineGroup)
int confirmationBars = input.int(2, "Closed bars to confirm / invalidate", minval=1, maxval=5, group=engineGroup)
int recoveryBars = input.int(2, "Supportive closes for recovery", minval=1, maxval=5, group=engineGroup)
float pullbackDeadband = input.float(0.10, "Pullback momentum deadband", minval=0.01, maxval=0.50, step=0.01, group=engineGroup, tooltip="Eight-bar price change divided by ATR × sqrt(8). A pullback needs opposing momentum beyond this threshold or opposing fast slopes. Recovery needs supportive momentum and full trend evidence.")
string visualGroup = "Appearance"
bool showFill = input.bool(true, "Fill between strands", group=visualGroup)
bool showPanel = input.bool(true, "Show dashboard", group=visualGroup)
int ribbonWidth = input.int(2, "Strand thickness", minval=1, maxval=3, group=visualGroup)
int startLength = preset == "Fast" ? 4 : preset == "Patient" ? 12 : 8
int stride = preset == "Fast" ? 2 : preset == "Patient" ? 6 : 4
int longestLength = startLength + stride * 59
float atr = ta.atr(14)
float unit = math.max(nz(atr, syminfo.mintick), syminfo.mintick)
f_clip(float value, float lo, float hi) =>
math.max(lo, math.min(hi, value))
f_efficiency(simple int n) =>
float travel = math.sum(math.abs(ta.change(close)), n)
not na(close[n]) and travel > 0 ? f_clip(math.abs(close - close[n]) / travel, 0.0, 1.0) : 0.0
f_vote(float value, float scale) =>
float delta = value - value[slopeBars]
float threshold = scale * slopeFloor * slopeBars
na(delta) ? 0.0 : delta > threshold ? 1.0 : delta < -threshold ? -1.0 : 0.0
// Each written call owns separate history. Do not move f_bank into a loop.
f_bank(simple int n, float scale) =>
float er = f_efficiency(n)
float ka = math.pow(er * (2.0 / 3.0 - 2.0 / 31.0) + 2.0 / 31.0, 2)
var float kama = na
kama := na(kama[1]) ? close : kama[1] + ka * (close - kama[1])
int half = int(n / 2)
float recentHigh = ta.highest(high, half)
float recentLow = ta.lowest(low, half)
float recentRange = recentHigh - recentLow
float olderRange = recentHigh[half] - recentLow[half]
float wholeRange = ta.highest(high, n) - ta.lowest(low, n)
float fa = 0.01
if bar_index >= n - 1 and wholeRange > 0
float rangeSum = recentRange + olderRange
float dimension = rangeSum > 0 ? math.log(2.0 * rangeSum / wholeRange) / math.log(2.0) : 1.0
fa := f_clip(math.exp(-4.6 * (dimension - 1.0)), 0.01, 1.0)
var float frama = na
frama := na(frama[1]) ? close : frama[1] + fa * (close - frama[1])
// Explicit integer convention: even half length, rounded square-root length.
float hull = ta.wma(2.0 * ta.wma(close, half) - ta.wma(close, n), int(math.round(math.sqrt(n))))
float ema = ta.ema(close, n)
float blend = adaptiveShare * (kama + frama) / 2.0 + (1.0 - adaptiveShare) * (hull + ema) / 2.0
float votes = adaptiveShare * (f_vote(kama, scale) + f_vote(frama, scale)) / 2.0 + (1.0 - adaptiveShare) * (f_vote(hull, scale) + f_vote(ema, scale)) / 2.0
[blend, votes]
// BEGIN GENERATED HORIZONS
[b00, v00] = f_bank(startLength + stride * 0, unit)
[b01, v01] = f_bank(startLength + stride * 1, unit)
[b02, v02] = f_bank(startLength + stride * 2, unit)
[b03, v03] = f_bank(startLength + stride * 3, unit)
[b04, v04] = f_bank(startLength + stride * 4, unit)
[b05, v05] = f_bank(startLength + stride * 5, unit)
[b06, v06] = f_bank(startLength + stride * 6, unit)
[b07, v07] = f_bank(startLength + stride * 7, unit)
[b08, v08] = f_bank(startLength + stride * 8, unit)
[b09, v09] = f_bank(startLength + stride * 9, unit)
[b10, v10] = f_bank(startLength + stride * 10, unit)
[b11, v11] = f_bank(startLength + stride * 11, unit)
[b12, v12] = f_bank(startLength + stride * 12, unit)
[b13, v13] = f_bank(startLength + stride * 13, unit)
[b14, v14] = f_bank(startLength + stride * 14, unit)
[b15, v15] = f_bank(startLength + stride * 15, unit)
[b16, v16] = f_bank(startLength + stride * 16, unit)
[b17, v17] = f_bank(startLength + stride * 17, unit)
[b18, v18] = f_bank(startLength + stride * 18, unit)
[b19, v19] = f_bank(startLength + stride * 19, unit)
[b20, v20] = f_bank(startLength + stride * 20, unit)
[b21, v21] = f_bank(startLength + stride * 21, unit)
[b22, v22] = f_bank(startLength + stride * 22, unit)
[b23, v23] = f_bank(startLength + stride * 23, unit)
[b24, v24] = f_bank(startLength + stride * 24, unit)
[b25, v25] = f_bank(startLength + stride * 25, unit)
[b26, v26] = f_bank(startLength + stride * 26, unit)
[b27, v27] = f_bank(startLength + stride * 27, unit)
[b28, v28] = f_bank(startLength + stride * 28, unit)
[b29, v29] = f_bank(startLength + stride * 29, unit)
[b30, v30] = f_bank(startLength + stride * 30, unit)
[b31, v31] = f_bank(startLength + stride * 31, unit)
[b32, v32] = f_bank(startLength + stride * 32, unit)
[b33, v33] = f_bank(startLength + stride * 33, unit)
[b34, v34] = f_bank(startLength + stride * 34, unit)
[b35, v35] = f_bank(startLength + stride * 35, unit)
[b36, v36] = f_bank(startLength + stride * 36, unit)
[b37, v37] = f_bank(startLength + stride * 37, unit)
[b38, v38] = f_bank(startLength + stride * 38, unit)
[b39, v39] = f_bank(startLength + stride * 39, unit)
[b40, v40] = f_bank(startLength + stride * 40, unit)
[b41, v41] = f_bank(startLength + stride * 41, unit)
[b42, v42] = f_bank(startLength + stride * 42, unit)
[b43, v43] = f_bank(startLength + stride * 43, unit)
[b44, v44] = f_bank(startLength + stride * 44, unit)
[b45, v45] = f_bank(startLength + stride * 45, unit)
[b46, v46] = f_bank(startLength + stride * 46, unit)
[b47, v47] = f_bank(startLength + stride * 47, unit)
[b48, v48] = f_bank(startLength + stride * 48, unit)
[b49, v49] = f_bank(startLength + stride * 49, unit)
[b50, v50] = f_bank(startLength + stride * 50, unit)
[b51, v51] = f_bank(startLength + stride * 51, unit)
[b52, v52] = f_bank(startLength + stride * 52, unit)
[b53, v53] = f_bank(startLength + stride * 53, unit)
[b54, v54] = f_bank(startLength + stride * 54, unit)
[b55, v55] = f_bank(startLength + stride * 55, unit)
[b56, v56] = f_bank(startLength + stride * 56, unit)
[b57, v57] = f_bank(startLength + stride * 57, unit)
[b58, v58] = f_bank(startLength + stride * 58, unit)
[b59, v59] = f_bank(startLength + stride * 59, unit)
float s00 = (b00 + b01 + b02) / 3.0
float a00 = (v00 + v01 + v02) / 3.0
float s01 = (b03 + b04 + b05) / 3.0
float a01 = (v03 + v04 + v05) / 3.0
float s02 = (b06 + b07 + b08) / 3.0
float a02 = (v06 + v07 + v08) / 3.0
float s03 = (b09 + b10 + b11) / 3.0
float a03 = (v09 + v10 + v11) / 3.0
float s04 = (b12 + b13 + b14) / 3.0
float a04 = (v12 + v13 + v14) / 3.0
float s05 = (b15 + b16 + b17) / 3.0
float a05 = (v15 + v16 + v17) / 3.0
float s06 = (b18 + b19 + b20) / 3.0
float a06 = (v18 + v19 + v20) / 3.0
float s07 = (b21 + b22 + b23) / 3.0
float a07 = (v21 + v22 + v23) / 3.0
float s08 = (b24 + b25 + b26) / 3.0
float a08 = (v24 + v25 + v26) / 3.0
float s09 = (b27 + b28 + b29) / 3.0
float a09 = (v27 + v28 + v29) / 3.0
float s10 = (b30 + b31 + b32) / 3.0
float a10 = (v30 + v31 + v32) / 3.0
float s11 = (b33 + b34 + b35) / 3.0
float a11 = (v33 + v34 + v35) / 3.0
float s12 = (b36 + b37 + b38) / 3.0
float a12 = (v36 + v37 + v38) / 3.0
float s13 = (b39 + b40 + b41) / 3.0
float a13 = (v39 + v40 + v41) / 3.0
float s14 = (b42 + b43 + b44) / 3.0
float a14 = (v42 + v43 + v44) / 3.0
float s15 = (b45 + b46 + b47) / 3.0
float a15 = (v45 + v46 + v47) / 3.0
float s16 = (b48 + b49 + b50) / 3.0
float a16 = (v48 + v49 + v50) / 3.0
float s17 = (b51 + b52 + b53) / 3.0
float a17 = (v51 + v52 + v53) / 3.0
float s18 = (b54 + b55 + b56) / 3.0
float a18 = (v54 + v55 + v56) / 3.0
float s19 = (b57 + b58 + b59) / 3.0
float a19 = (v57 + v58 + v59) / 3.0
array<float> strands = array.from(s00, s01, s02, s03, s04, s05, s06, s07, s08, s09, s10, s11, s12, s13, s14, s15, s16, s17, s18, s19)
array<float> votes = array.from(a00, a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)
// END GENERATED HORIZONS
float broadVote = array.avg(votes)
float fastVote = 0.0
float slowVote = 0.0
float orderVote = 0.0
for i = 0 to 19
if i < 7
fastVote += array.get(votes, i) / 7.0
if i >= 13
slowVote += array.get(votes, i) / 7.0
if i < 19
float spacing = array.get(strands, i) - array.get(strands, i + 1)
orderVote += (spacing > unit * 0.01 ? 1.0 : spacing < -unit * 0.01 ? -1.0 : 0.0) / 19.0
f_roc(simple int n) =>
not na(close[n]) and close[n] != 0 ? 100.0 * (close - close[n]) / math.abs(close[n]) : 0.0
float roc8 = f_roc(8)
float roc21 = f_roc(21)
float roc55 = f_roc(55)
float fastMove = (close - close[8]) / (unit * math.sqrt(8.0))
float atrPercent = 100.0 * unit / math.max(math.abs(close), syminfo.mintick)
float momentum = (f_clip(roc8 / (atrPercent * math.sqrt(8.0)), -1.0, 1.0) + f_clip(roc21 / (atrPercent * math.sqrt(21.0)), -1.0, 1.0) + f_clip(roc55 / (atrPercent * math.sqrt(55.0)), -1.0, 1.0)) / 3.0
float efficiency = f_efficiency(21)
float spread = (array.max(strands) - array.min(strands)) / unit
float meanSpread = ta.sma(spread, 100)
bool compressed = spread < 0.40 or (not na(meanSpread) and spread < meanSpread * 0.65)
float smoothSpread = ta.ema(spread, 5)
float spreadChange = (smoothSpread - smoothSpread[3]) / 3.0
string widthMotion = spreadChange > 0.01 ? "Expanding" : spreadChange < -0.01 ? "Contracting" : "Steady"
bool ready = bar_index >= longestLength * 3 and not na(s19) and not na(atr) and not na(meanSpread)
// State and dashboard evidence are committed only when the bar closes.
var int direction = 0
var int pending = 0
var int streak = 0
var int weakBars = 0
var bool inPullback = false
var int recoveryStreak = 0
var string state = "Warming up"
var string health = "Warming up"
var string closedStructure = "Warming up"
var float closedAgreement = na
var float closedMomentum = na
var float closedSpread = na
var float closedEfficiency = na
if barstate.isconfirmed
if not ready
direction := 0
pending := 0
streak := 0
weakBars := 0
inPullback := false
recoveryStreak := 0
state := "Warming up"
health := "Warming up"
closedStructure := "Warming up"
closedAgreement := na
closedMomentum := na
closedSpread := na
closedEfficiency := na
else
int previousDirection = direction
int candidate = 0
if slowVote >= 0.50 and broadVote >= 0.35 and orderVote >= 0.20 and momentum > 0.10 and efficiency >= 0.20
candidate := 1
else if slowVote <= -0.50 and broadVote <= -0.35 and orderVote <= -0.20 and momentum < -0.10 and efficiency >= 0.20
candidate := -1
streak := candidate != 0 and candidate == pending ? streak + 1 : candidate != 0 ? 1 : 0
pending := candidate
bool weak = direction != 0 and slowVote * direction < 0.15
weakBars := weak ? weakBars + 1 : 0
if weakBars >= confirmationBars
direction := 0
weakBars := 0
if candidate != 0 and streak >= confirmationBars
direction := candidate
weakBars := 0
if direction == 0 or direction != previousDirection
inPullback := false
recoveryStreak := 0
if direction == 0
health := "Unestablished"
else if slowVote * direction < 0.35
health := "Weakening"
else if compressed or efficiency < 0.20
health := "Stalled"
else
health := candidate == direction ? "Strong" : "Weakening"
if direction != 0
if slowVote * direction < 0.35
inPullback := false
recoveryStreak := 0
state := "Fracturing"
else
bool adverse = fastVote * direction < -0.10 or fastMove * direction < -pullbackDeadband
bool supportive = fastVote * direction >= 0.10 and fastMove * direction > pullbackDeadband and health == "Strong"
if adverse
inPullback := true
recoveryStreak := inPullback and supportive ? recoveryStreak + 1 : 0
if recoveryStreak >= recoveryBars
inPullback := false
recoveryStreak := 0
state := inPullback ? "Pullback" : health == "Strong" ? "Trending" : health
else
state := compressed ? "Compression" : math.abs(fastVote) >= 0.40 and fastVote * fastMove > 0 and efficiency >= 0.20 ? "Building" : "Mixed"
closedAgreement := broadVote * 100.0
closedMomentum := momentum * 100.0
closedSpread := spread
closedEfficiency := efficiency * 100.0
closedStructure := (compressed ? "Compressed · " : "") + widthMotion
bool eventGate = barstate.isconfirmed and ready and ready[1]
bool bullEvent = eventGate and direction == 1 and direction[1] != 1
bool bearEvent = eventGate and direction == -1 and direction[1] != -1
bool recoveryEvent = eventGate and direction != 0 and direction == direction[1] and state[1] == "Pullback" and state == "Trending"
bool fractureEvent = eventGate and state == "Fracturing" and state[1] != "Fracturing"
// Hue identifies horizon. Agreement/momentum/efficiency control brightness.
f_hue(simple int index) =>
float x = float(index)
x <= 4 ? color.from_gradient(x, 0, 4, #FFD166, #FF7043) : x <= 8 ? color.from_gradient(x, 4, 8, #FF7043, #EF5DA8) : x <= 12 ? color.from_gradient(x, 8, 12, #EF5DA8, #A775FF) : x <= 16 ? color.from_gradient(x, 12, 16, #A775FF, #548CFF) : color.from_gradient(x, 16, 19, #548CFF, #4DE3D0)
f_tint(simple int index, float agreement, bool filling) =>
float support = math.max(0.0, math.sign(agreement) * momentum)
float quality = ready ? f_clip(0.60 * math.abs(agreement) + 0.25 * support + 0.15 * efficiency, 0.0, 1.0) : 0.0
color tint = color.from_gradient(quality, 0.0, 1.0, #737A90, f_hue(index))
float transparency = filling ? 92.0 - 24.0 * quality : 68.0 - 58.0 * quality
color.new(tint, transparency)
// BEGIN GENERATED VISUALS
p00 = plot(s00, "Spectrum 01", color=f_tint(0, a00, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p01 = plot(s01, "Spectrum 02", color=f_tint(1, a01, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p02 = plot(s02, "Spectrum 03", color=f_tint(2, a02, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p03 = plot(s03, "Spectrum 04", color=f_tint(3, a03, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p04 = plot(s04, "Spectrum 05", color=f_tint(4, a04, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p05 = plot(s05, "Spectrum 06", color=f_tint(5, a05, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p06 = plot(s06, "Spectrum 07", color=f_tint(6, a06, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p07 = plot(s07, "Spectrum 08", color=f_tint(7, a07, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p08 = plot(s08, "Spectrum 09", color=f_tint(8, a08, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p09 = plot(s09, "Spectrum 10", color=f_tint(9, a09, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p10 = plot(s10, "Spectrum 11", color=f_tint(10, a10, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p11 = plot(s11, "Spectrum 12", color=f_tint(11, a11, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p12 = plot(s12, "Spectrum 13", color=f_tint(12, a12, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p13 = plot(s13, "Spectrum 14", color=f_tint(13, a13, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p14 = plot(s14, "Spectrum 15", color=f_tint(14, a14, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p15 = plot(s15, "Spectrum 16", color=f_tint(15, a15, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p16 = plot(s16, "Spectrum 17", color=f_tint(16, a16, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p17 = plot(s17, "Spectrum 18", color=f_tint(17, a17, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p18 = plot(s18, "Spectrum 19", color=f_tint(18, a18, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p19 = plot(s19, "Spectrum 20", color=f_tint(19, a19, false), linewidth=ribbonWidth, editable=false, display=display.pane)
fill(p00, p01, color=showFill ? f_tint(0, (a00 + a01) / 2.0, true) : na, title="Weave 01", editable=false)
fill(p01, p02, color=showFill ? f_tint(1, (a01 + a02) / 2.0, true) : na, title="Weave 02", editable=false)
fill(p02, p03, color=showFill ? f_tint(2, (a02 + a03) / 2.0, true) : na, title="Weave 03", editable=false)
fill(p03, p04, color=showFill ? f_tint(3, (a03 + a04) / 2.0, true) : na, title="Weave 04", editable=false)
fill(p04, p05, color=showFill ? f_tint(4, (a04 + a05) / 2.0, true) : na, title="Weave 05", editable=false)
fill(p05, p06, color=showFill ? f_tint(5, (a05 + a06) / 2.0, true) : na, title="Weave 06", editable=false)
fill(p06, p07, color=showFill ? f_tint(6, (a06 + a07) / 2.0, true) : na, title="Weave 07", editable=false)
fill(p07, p08, color=showFill ? f_tint(7, (a07 + a08) / 2.0, true) : na, title="Weave 08", editable=false)
fill(p08, p09, color=showFill ? f_tint(8, (a08 + a09) / 2.0, true) : na, title="Weave 09", editable=false)
fill(p09, p10, color=showFill ? f_tint(9, (a09 + a10) / 2.0, true) : na, title="Weave 10", editable=false)
fill(p10, p11, color=showFill ? f_tint(10, (a10 + a11) / 2.0, true) : na, title="Weave 11", editable=false)
fill(p11, p12, color=showFill ? f_tint(11, (a11 + a12) / 2.0, true) : na, title="Weave 12", editable=false)
fill(p12, p13, color=showFill ? f_tint(12, (a12 + a13) / 2.0, true) : na, title="Weave 13", editable=false)
fill(p13, p14, color=showFill ? f_tint(13, (a13 + a14) / 2.0, true) : na, title="Weave 14", editable=false)
fill(p14, p15, color=showFill ? f_tint(14, (a14 + a15) / 2.0, true) : na, title="Weave 15", editable=false)
fill(p15, p16, color=showFill ? f_tint(15, (a15 + a16) / 2.0, true) : na, title="Weave 16", editable=false)
fill(p16, p17, color=showFill ? f_tint(16, (a16 + a17) / 2.0, true) : na, title="Weave 17", editable=false)
fill(p17, p18, color=showFill ? f_tint(17, (a17 + a18) / 2.0, true) : na, title="Weave 18", editable=false)
fill(p18, p19, color=showFill ? f_tint(18, (a18 + a19) / 2.0, true) : na, title="Weave 19", editable=false)
// END GENERATED VISUALS
// 20 plots × 2 counts + 19 series fills + 4 alertconditions = 63 plot counts.
alertcondition(bullEvent, "Bull trend established", "Konatus Spectrum: confirmed bullish trend on {{ticker}} {{interval}}. Close: {{close}}")
alertcondition(bearEvent, "Bear trend established", "Konatus Spectrum: confirmed bearish trend on {{ticker}} {{interval}}. Close: {{close}}")
alertcondition(recoveryEvent, "Pullback recovery", "Konatus Spectrum: confirmed pullback recovery on {{ticker}} {{interval}}. Check chart direction. Close: {{close}}")
alertcondition(fractureEvent, "Trend fracturing", "Konatus Spectrum: slow trend agreement is deteriorating on {{ticker}} {{interval}}. Close: {{close}}")
var table panel = table.new(position.top_right, 2, 11, bgcolor=color.new(#111725, 8), frame_color=#303B52, frame_width=1, border_width=0)
if barstate.islast and showPanel
color accent = direction > 0 ? #4DE3D0 : direction < 0 ? #EF5DA8 : #FFD166
string bias = direction > 0 ? "BULL" : direction < 0 ? "BEAR" : "MIXED"
string phase = state == "Pullback" and recoveryStreak > 0 ? state + " (" + str.tostring(recoveryStreak) + "/" + str.tostring(recoveryBars) + ")" : state
string agreementText = na(closedAgreement) ? "—" : (closedAgreement > 0 ? "+" : "") + str.tostring(closedAgreement, "#.0")
string momentumText = na(closedMomentum) ? "—" : (closedMomentum > 0 ? "+" : "") + str.tostring(closedMomentum, "#.0")
string chartNote = not chart.is_standard ? "Use standard candles" : not timeframe.isintraday ? "Designed for intraday" : "Last closed bar"
table.cell(panel, 0, 0, "KONATUS", text_color=#F1EEE8, text_size=size.small)
table.cell(panel, 1, 0, "SPECTRUM", text_color=#A775FF, text_size=size.small)
table.cell(panel, 0, 1, "Trend", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 1, bias + " · " + phase, text_color=accent, text_size=size.small)
table.cell(panel, 0, 2, "Trend health", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 2, health, text_color=health == "Strong" ? #4DE3D0 : health == "Unestablished" or health == "Warming up" ? #A6AFC0 : #FFD166, text_size=size.small)
table.cell(panel, 0, 3, "Structure", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 3, closedStructure, text_color=#E1E6F0, text_size=size.small)
table.cell(panel, 0, 4, "Slope balance", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 4, agreementText + " / ±100", text_color=#E1E6F0, text_size=size.small)
table.cell(panel, 0, 5, "ROC blend", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 5, momentumText + " / ±100", text_color=#E1E6F0, text_size=size.small)
table.cell(panel, 0, 6, "Ribbon width", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 6, na(closedSpread) ? "—" : str.tostring(closedSpread, "#.00") + " ATR", text_color=#E1E6F0, text_size=size.small)
table.cell(panel, 0, 7, "Efficiency", text_color=#A6AFC0, text_size=size.small)
table.cell(panel, 1, 7, na(closedEfficiency) ? "—" : str.tostring(closedEfficiency, "#.0") + "%", text_color=#E1E6F0, text_size=size.small)
table.cell(panel, 0, 8, "240 MAs · 20 strands", text_color=#A6AFC0, text_size=size.tiny)
table.cell(panel, 1, 8, preset + " · " + str.tostring(adaptiveShare * 100, "#") + "% adaptive", text_color=#A6AFC0, text_size=size.tiny)
table.cell(panel, 0, 9, "kuttystauder.com", text_color=#737F96, text_size=size.tiny)
table.cell(panel, 1, 9, chartNote, text_color=#737F96, text_size=size.tiny)
table.cell(panel, 0, 10, "Live ribbon", text_color=#737F96, text_size=size.tiny)
table.cell(panel, 1, 10, state == "Warming up" ? str.tostring(math.max(0, longestLength * 3 - bar_index)) + " bars to warmup" : "Scores are not probabilities", text_color=#737F96, text_size=size.tiny)
More averages and adaptive behavior do not establish better net returns. Read the trading and research disclaimer ↓
09 / Failed and sustained breakouts · Pine Script v6 · Research release
Price left the range.
Did the market agree?
RAR stands for Range Acceptance & Rejection. Most breakout tools fire when price crosses a line. This one waits to see what happens next. It freezes a consolidation range, then separates two events: a rejection, where price probes past a boundary and closes back inside, and an acceptance, where price closes beyond it twice, holds a retest and continues. Each finished setup carries an entry reference, a stop, a structural target and an R multiple.

Research prototype 0.3.0. All 27 reference-model scenarios and the source audits pass, and TradingView’s own translator accepts this exact source with zero errors and zero warnings. It has not yet been run on a live TradingView chart, so drawing, Bar Replay and alert delivery remain unverified. The study below comes from the Python model, not the Pine script. No edge has been established. At default settings, results before costs were near zero or negative.
Three jobs, in order
The last 20 completed candles qualify when their high-low span is at most 3 ATR and each boundary has two touches at least 3 bars apart. The current bar must still close inside. Boundaries, ATR and buffers then stop moving. Manual mode uses your own levels from a chosen time onward.
A breach means trading past a boundary by more than a buffer of 0.10 ATR, or one tick if that is larger. Every decision waits for a closed candle. Each range gets one completed signal, then retires and must be replaced by a window of entirely new bars.
Risk runs from the signal close to a stop beyond the structure that would prove the idea wrong. Reward runs to a target that already existed. A setup that reaches your minimum R, 2.0 by default, draws a triangle. One that does not draws a muted circle, so the filter never hides what happened.
The two events
Stop: 2 ticks beyond the most extreme price of the whole excursion. Target: the opposite boundary.
Stop: 2 ticks beyond the extreme since the retest. Target: the nearest swing pivot past the entry that was already confirmed before the signal bar.
An acceptance can turn into a rejection. If price closes back inside after it armed, that close starts the opposite rejection with the whole run behind its stop. A candle breaching both boundaries cancels any live setup and starts nothing.
Read the chart
Signals, levels and alerts are set on closed candles and do not move afterward. Only a settings change or a data revision recalculates history. The indicator uses price alone, with no volume, footprint or order-flow input, so “acceptance” and “rejection” describe what price did, not who traded. It borrows its vocabulary from auction-market thinking, but it is not a volume profile, not a fair-value calculation and not any named trader’s strategy.
Evidence
The reference model ran over about 1.13 million real bars at default settings, and no parameter was changed afterward. Each valid setup entered at its signal close and ended at the first touch of stop or target. A bar touching both counted as the stop, and a gap through the stop filled at the open. Commissions, spread and slippage were left out, so a real result would be lower.
| Setup | Resolved | Reached target | Needed to break even | Median planned R | Mean R per setup |
|---|---|---|---|---|---|
| Daily, 20 stocks and ETFs, 2010 to September 2026: 82,797 bars, 58 signals | |||||
| Rejections | 42 | 61.9% | 63.8% | 0.56R | −0.064R (−0.322 to +0.192) |
| Acceptances | 14 | 71.4% | 80.9% | 0.19R | −0.159R (−0.450 to +0.129) |
| Qualified at 2R, both types | 3 | 33.3% | 29.1% | 2.58R | +0.033R (−1.000 to +1.065) |
| Hourly, 5 crypto pairs, October 2019 to September 2026: 276,757 bars, 338 signals | |||||
| Rejections | 273 | 61.2% | 57.2% | 0.80R | +0.056R (−0.029 to +0.140) |
| Acceptances | 64 | 70.3% | 77.2% | 0.25R | −0.077R (−0.198 to +0.045) |
| Qualified at 2R, both types | 24 | 25.0% | 28.2% | 2.41R | −0.188R (−0.608 to +0.271) |
| 5-minute, AAPL AMZN META NFLX, regular session, 2016 to September 2026: 770,070 bars, 1,851 signals | |||||
| Rejections | 1,448 | 59.5% | 59.1% | 0.76R | −0.063R (−0.111 to −0.014) |
| Acceptances | 348 | 69.5% | 75.1% | 0.21R | −0.114R (−0.185 to −0.042) |
| Qualified at 2R, both types | 135 | 28.9% | 27.7% | 2.41R | −0.100R (−0.349 to +0.168) |
Read plainly, this version labels what price did at a range. It does not show that trading those labels pays. Other instruments, session filters or exits might behave differently, but finding that out needs its own test on data kept separate from this study. These instruments are also correlated, and the results are hypothetical.
From 0.1.0 to 0.2.0
The first version passed all of its own tests. Reading the code against its documentation found these defects, and each logic defect was first reproduced with a failing test. All are fixed. No default changed.
The four duplicated signal blocks also became one, so a future fix cannot land in three of them and miss the fourth.
Version 0.3.0 changes the display only. Thick labelled lines replace the thin box and short dotted levels, and markers and text are larger. Signals, stops and targets are unchanged, so the study results above still apply.
From source to chart
VALIDATION.md, included in the download. Anything that differs from a row is a defect worth reporting.These are initial research values, not optimized ones. Limits: chart timeframe only, one active range at a time, one signal per range, no volume or order-flow input.
Version 0.3.0 · Complete Pine Script v6 source
//@version=6
// Konatus RAR 0.3.0 (Range Acceptance & Rejection)
// An experimental implementation of auction-market concepts: established range,
// failed breakout (rejection), sustained breakout (acceptance), and structural
// reward relative to risk.
//
// This is not Patrick Nill's strategy and no edge is claimed. All numerical
// defaults are initial research parameters, not optimised values.
//
// Reliability contract:
// - every state change and every signal is evaluated on confirmed bars only
// - no negative plot offsets, no intrabar assumptions, no higher timeframes
// - boundaries, ATR, tolerance and buffer freeze at range activation
// - one active range, one completed signal per range
// - confirmed history does not move when later bars print; a settings change
// or a data revision can still recalculate it
indicator("Konatus RAR", "Konatus RAR", overlay = true,
max_bars_back = 500, max_boxes_count = 100, max_lines_count = 500,
max_labels_count = 500)
//==================================================================== inputs
string G_RANGE = "1. Range identification"
string G_SETUP = "2. Setup windows"
string G_RISK = "3. Risk and target"
string G_DISP = "4. Display"
string G_ALERT = "5. Alerts"
iMode = input.string("Automatic", "Mode", options = ["Automatic", "Manual"], group = G_RANGE,
tooltip = "Automatic derives the range from recent price. Manual uses the levels below, from a separate profile study.")
iN = input.int(20, "Formation window (bars)", minval = 5, maxval = 200, group = G_RANGE,
tooltip = "Initial research parameter. Number of completed candles whose highest high and lowest low define the range.")
iAtrLen = input.int(14, "ATR length", minval = 2, maxval = 100, group = G_RANGE)
iMaxWidth = input.float(3.0, "Max range width (x ATR)", minval = 0.5, maxval = 10.0, step = 0.1, group = G_RANGE,
tooltip = "Initial research parameter. Rejects a formation window wider than this multiple of ATR.")
iTolAtr = input.float(0.15, "Boundary touch tolerance (x ATR)", minval = 0.01, maxval = 1.0, step = 0.01, group = G_RANGE,
tooltip = "Initial research parameter. A touch is a high or low within this distance of the boundary.")
iTouches = input.int(2, "Required touches per boundary", minval = 1, maxval = 10, group = G_RANGE)
iTouchGap = input.int(3, "Minimum bars between touches", minval = 1, maxval = 20, group = G_RANGE)
iRangeLife = input.int(100, "Automatic range life (bars)", minval = 10, maxval = 2000, group = G_RANGE,
tooltip = "Initial research parameter. Age at which an automatic range retires. Manual ranges do not age out.")
iManHi = input.float(0.0, "Manual upper boundary", group = G_RANGE)
iManLo = input.float(0.0, "Manual lower boundary", group = G_RANGE)
iManTs = input.time(timestamp("01 Jan 2024 00:00 +0000"), "Manual activation time", group = G_RANGE,
tooltip = "Manual levels are inert on every bar before this timestamp.")
iBufAtr = input.float(0.10, "Breakout buffer (x ATR)", minval = 0.0, maxval = 2.0, step = 0.01, group = G_SETUP,
tooltip = "Initial research parameter. Effective buffer is the larger of one tick and this multiple of frozen ATR.")
iReclaimWin = input.int(3, "Reclaim window after breach (bars)", minval = 0, maxval = 10, group = G_SETUP,
tooltip = "A breach may reclaim on the same candle or within this many following candles.")
iConfirmWin = input.int(5, "Confirmation window (bars)", minval = 1, maxval = 20, group = G_SETUP,
tooltip = "Bars allowed after the reclaim or retest candle for the confirmation close.")
iRetestWin = input.int(5, "Retest window after acceptance (bars)", minval = 1, maxval = 20, group = G_SETUP)
iPivL = input.int(3, "Pivot left bars", minval = 1, maxval = 50, group = G_RISK,
tooltip = "Confirmed swing definition. A pivot only becomes eligible after its right-side bars have printed.")
iPivR = input.int(3, "Pivot right bars", minval = 1, maxval = 50, group = G_RISK)
iStopTicks = input.int(2, "Stop buffer (ticks)", minval = 0, maxval = 100, group = G_RISK)
iMinR = input.float(2.0, "Minimum R to qualify", minval = 0.1, maxval = 20.0, step = 0.1, group = G_RISK,
tooltip = "Initial research parameter. Signals below this multiple are still marked, in a muted style.")
iProject = input.bool(false, "Draw 2R and 3R projection lines", group = G_RISK,
tooltip = "Projections are labelled as projections and are never used as the structural target.")
iRangeColor = input.color(#FFB300, "Range line color", group = G_DISP)
iRangeWidth = input.int(3, "Range line thickness", minval = 1, maxval = 5, group = G_DISP)
iLevelWidth = input.int(2, "Entry, stop and target thickness", minval = 1, maxval = 5, group = G_DISP)
iLevelBars = input.int(15, "Entry, stop and target length (bars)", minval = 5, maxval = 100, group = G_DISP)
iTextSize = input.string("Small", "Text size", options = ["Small", "Normal", "Large"], group = G_DISP)
iShowRangeLbl = input.bool(true, "Range high and low price labels", group = G_DISP)
iShowLbl = input.bool(true, "Signal labels", group = G_DISP)
iShowLines = input.bool(true, "Entry, stop and target lines", group = G_DISP)
iShowDash = input.bool(true, "Dashboard", group = G_DISP)
iShowMid = input.bool(true, "Range midpoint", group = G_DISP)
iShowScore = input.bool(true, "Scoreboard rows in the dashboard", group = G_DISP,
tooltip = "First touch of stop or target for each valid signal on this chart. Hypothetical fills at the signal close, no costs. A bar touching both counts as the stop.")
iScoreQual = input.bool(true, "Scoreboard counts qualified signals only", group = G_DISP)
string txtSize = iTextSize == "Large" ? size.large : iTextSize == "Normal" ? size.normal : size.small
iAlertQual = input.bool(false, "Alert only when minimum R is met", group = G_ALERT,
tooltip = "Applies to the four alert conditions and to the Any alert() function call stream.")
//=================================================================== palette
color C_RANGE = #787b86
color C_FILL = color.new(iRangeColor, 90)
color C_MID = color.new(iRangeColor, 30)
color C_BULL = #26a69a
color C_BEAR = #ef5350
color C_MUTED = #787b86
color C_ENTRY = #b2b5be
color C_STOP = #ef5350
color C_TARGET = #26a69a
color C_TEXT = #d1d4dc
color C_BG = color.new(#131722, 10)
color C_PANEL = color.new(#131722, 15)
//============================================================== global series
float mintick = syminfo.mintick
float atr = ta.atr(iAtrLen)
float hhN = ta.highest(high, iN)[1]
float llN = ta.lowest(low, iN)[1]
float atrRef = atr[1]
float ph = ta.pivothigh(high, iPivL, iPivR)
float pl = ta.pivotlow(low, iPivL, iPivR)
//=============================================================== state storage
// range state: 0 seeking, 1 active
var int rstate = 0
var float rHi = na
var float rLo = na
var float rAtr = na
var float rTol = na
var float rBuf = na
var int rAct = -1
var int lastRetire = -100000
var string lastRetireWhy = ""
// setup state: 0 none, 1 bull rejection awaiting reclaim, 2 bull rejection pending,
// 3 bear rejection awaiting reclaim, 4 bear rejection pending,
// 5 bull acceptance awaiting retest, 6 bull acceptance pending,
// 7 bear acceptance awaiting retest, 8 bear acceptance pending
var int ss = 0
var int pBar = -1
var int pExp = -1
var float exLow = na
var float exHigh = na
var float rcHigh = na
var float rcLow = na
var bool accBReady = true
var bool accSReady = true
var float outHi = na // highest high above the range in this excursion
var float outLo = na // lowest low below the range in this excursion
var bool upUsed = false // this excursion has already spent its reclaim window
var bool dnUsed = false
var bool signalled = false
var bool manualDone = false
// pivots confirmed on or before the current bar
var array<float> pivHiPrice = array.new<float>()
var array<int> pivHiKnown = array.new<int>()
var array<float> pivLoPrice = array.new<float>()
var array<int> pivLoKnown = array.new<int>()
// signals awaiting first touch of stop or target, and the running score
var array<float> opEntry = array.new<float>()
var array<float> opStop = array.new<float>()
var array<float> opTarget = array.new<float>()
var array<float> opRisk = array.new<float>()
var array<float> opPlanR = array.new<float>()
var array<bool> opLong = array.new<bool>()
var array<bool> opAcc = array.new<bool>()
var int rejWins = 0
var int rejLoss = 0
var float rejSumR = 0.0
var int accWins = 0
var int accLoss = 0
var float accSumR = 0.0
// drawings
var box rangeBox = na
var line midLine = na
var line hiLine = na
var line loLine = na
var label hiLabel = na
var label loLabel = na
var table dash = na
// per-bar signal: 0 none, 1 bull rejection, 2 bear rejection,
// 3 bull acceptance, 4 bear acceptance
int emit = 0
bool nowQual = false
// last completed signal, for the dashboard
var int lastSigBar = -1
var string lastSigKind = "none"
var float lastSigR = na
var bool lastSigQual = false
var string lastSigWhy = ""
//================================================================== utilities
fmtPrice(float p) =>
na(p) ? "n/a" : str.tostring(p, format.mintick)
calcRisk(float entry, float stop, float target, bool isLong) =>
float risk = math.abs(entry - stop)
bool ok = true
string why = ""
if na(target)
ok := false
why := "No structural target"
else if isLong and not (stop < entry and entry < target)
ok := false
why := "Invalid price ordering"
else if not isLong and not (stop > entry and entry > target)
ok := false
why := "Invalid price ordering"
else if risk <= 0
ok := false
why := "Zero risk"
float reward = ok ? math.abs(target - entry) : na
float rmult = ok ? reward / risk : na
[ok, risk, reward, rmult, why]
nearestPivot(float entry, bool isLong) =>
float best = na
if isLong
int n = array.size(pivHiPrice)
if n > 0
for k = 0 to n - 1
float p = array.get(pivHiPrice, k)
int known = array.get(pivHiKnown, k)
if known < bar_index and p > entry
best := na(best) ? p : math.min(best, p)
else
int m = array.size(pivLoPrice)
if m > 0
for k = 0 to m - 1
float p = array.get(pivLoPrice, k)
int known = array.get(pivLoKnown, k)
if known < bar_index and p < entry
best := na(best) ? p : math.max(best, p)
best
fmtR(float r) =>
na(r) ? "R n/a" : str.tostring(r, "#.##") + "R"
fmtScore(int wins, int losses, float sumR) =>
int n = wins + losses
n == 0 ? "no resolved signals" : str.tostring(wins) + " target, " + str.tostring(losses) + " stop, " + str.tostring(sumR / n, "+#.##;-#.##") + "R avg"
setupText(string kind, float entry, float stop, float target, float rmult, bool ok, string why, bool compact) =>
string s = kind
if not (compact and ok)
s := s + "\nentry " + fmtPrice(entry) + "\nstop " + fmtPrice(stop) + "\ntarget " + fmtPrice(target)
s := s + "\n" + fmtR(rmult)
if not ok
s := s + "\n" + why
s
//=================================================== confirmed-bar state machine
if barstate.isconfirmed
// 1. pivot bookkeeping. ta.pivothigh returns a value only on the bar that
// confirms the pivot, which is what keeps the target free of hindsight.
if not na(ph)
array.push(pivHiPrice, ph)
array.push(pivHiKnown, bar_index)
if array.size(pivHiPrice) > 100
array.shift(pivHiPrice)
array.shift(pivHiKnown)
if not na(pl)
array.push(pivLoPrice, pl)
array.push(pivLoKnown, bar_index)
if array.size(pivLoPrice) > 100
array.shift(pivLoPrice)
array.shift(pivLoKnown)
// 2. score earlier signals first, so a signal cannot score on its own bar.
// A bar touching both levels counts as the stop, because the order inside
// a bar is unknowable, and a gap through the stop fills at the open.
int nOpen = array.size(opStop)
if nOpen > 0
for k = nOpen - 1 to 0
bool tradeLong = array.get(opLong, k)
float tradeStop = array.get(opStop, k)
float tradeTgt = array.get(opTarget, k)
bool hitStop = tradeLong ? low <= tradeStop : high >= tradeStop
bool hitTgt = tradeLong ? high >= tradeTgt : low <= tradeTgt
if hitStop or hitTgt
float tradeEntry = array.get(opEntry, k)
float fillPx = tradeLong ? math.min(open, tradeStop) : math.max(open, tradeStop)
float moved = tradeLong ? fillPx - tradeEntry : tradeEntry - fillPx
float gotR = hitStop ? moved / array.get(opRisk, k) : array.get(opPlanR, k)
if array.get(opAcc, k)
accWins := accWins + (hitStop ? 0 : 1)
accLoss := accLoss + (hitStop ? 1 : 0)
accSumR := accSumR + gotR
else
rejWins := rejWins + (hitStop ? 0 : 1)
rejLoss := rejLoss + (hitStop ? 1 : 0)
rejSumR := rejSumR + gotR
array.remove(opEntry, k)
array.remove(opStop, k)
array.remove(opTarget, k)
array.remove(opRisk, k)
array.remove(opPlanR, k)
array.remove(opLong, k)
array.remove(opAcc, k)
// 3. range activation
if rstate == 0
float newHi = na
float newLo = na
float newAtr = na
int newLeft = bar_index
if iMode == "Automatic"
bool fresh = bar_index - lastRetire > iN
bool ready = bar_index >= iN + iAtrLen and not na(atrRef) and atrRef > 0
if fresh and ready and close >= llN and close <= hhN
float tolRef = iTolAtr * atrRef
int tHi = 0
int tLo = 0
int lastHi = -1
int lastLo = -1
for k = 1 to iN
int off = iN + 1 - k
if high[off] >= hhN - tolRef
if lastHi < 0 or math.abs(off - lastHi) >= iTouchGap
tHi := tHi + 1
lastHi := off
if low[off] <= llN + tolRef
if lastLo < 0 or math.abs(off - lastLo) >= iTouchGap
tLo := tLo + 1
lastLo := off
bool narrow = hhN - llN <= iMaxWidth * atrRef
bool both = tHi >= iTouches and tLo >= iTouches
if narrow and both and hhN > llN
newHi := hhN
newLo := llN
newAtr := atrRef
newLeft := bar_index - iN
else if not manualDone and iManHi > iManLo and not na(atr) and atr > 0 and time >= iManTs
manualDone := true
newHi := iManHi
newLo := iManLo
newAtr := atr
if not na(newHi)
rHi := newHi
rLo := newLo
rAtr := newAtr
rTol := iTolAtr * newAtr
rBuf := math.max(mintick, iBufAtr * newAtr)
rAct := bar_index
rstate := 1
ss := 0
signalled := false
accBReady := true
accSReady := true
outHi := na
outLo := na
upUsed := false
dnUsed := false
rangeBox := box.new(newLeft, rHi, bar_index, rLo,
border_color = color.new(iRangeColor, 100), border_width = 0, bgcolor = C_FILL,
xloc = xloc.bar_index)
hiLine := line.new(newLeft, rHi, bar_index, rHi, color = iRangeColor, width = iRangeWidth, xloc = xloc.bar_index)
loLine := line.new(newLeft, rLo, bar_index, rLo, color = iRangeColor, width = iRangeWidth, xloc = xloc.bar_index)
hiLabel := na
loLabel := na
if iShowRangeLbl
hiLabel := label.new(bar_index, rHi, "Range high " + fmtPrice(rHi), style = label.style_label_left, color = C_PANEL, textcolor = iRangeColor, size = txtSize)
loLabel := label.new(bar_index, rLo, "Range low " + fmtPrice(rLo), style = label.style_label_left, color = C_PANEL, textcolor = iRangeColor, size = txtSize)
if iShowMid
midLine := line.new(newLeft, (rHi + rLo) / 2, bar_index,
(rHi + rLo) / 2, color = C_MID, style = line.style_dotted,
xloc = xloc.bar_index)
else
midLine := na
label.new(bar_index, rLo, iMode == "Automatic" ? "range known" : "levels active",
style = label.style_label_up, color = C_PANEL, textcolor = C_TEXT, size = txtSize)
// 4. extend the frozen drawings while the range is live, including the bar
// on which it retires
bool wasLive = rstate == 1
if rstate == 1 and not na(rangeBox)
box.set_right(rangeBox, bar_index)
line.set_x2(hiLine, bar_index)
line.set_x2(loLine, bar_index)
if not na(midLine)
line.set_x2(midLine, bar_index)
if not na(hiLabel)
label.set_x(hiLabel, bar_index)
label.set_x(loLabel, bar_index)
// 5. evaluation, only after the activation bar
if rstate == 1 and not na(rHi)
if iMode == "Automatic" and (bar_index - rAct) > iRangeLife
rstate := 0
ss := 0
pBar := -1
pExp := -1
lastRetire := bar_index
lastRetireWhy := "range life exceeded"
else if bar_index > rAct
bool breachUp = high > rHi + rBuf
bool breachDn = low < rLo - rBuf
bool ambiguous = breachUp and breachDn
bool failUp = false
bool failDn = false
// excursion tracking: from the first trade beyond a boundary until a
// bar after a close back inside, whichever setup branch is live, so
// every rejection stop can clear all of it
if close[1] < rHi
outHi := na
if close[1] > rLo
outLo := na
if high > rHi
outHi := na(outHi) ? high : math.max(outHi, high)
if low < rLo
outLo := na(outLo) ? low : math.min(outLo, low)
// latches: a fresh two-close sequence is required after price returns
// to the boundary, and one excursion gets one reclaim window
if close <= rHi + rBuf
accBReady := true
if close >= rLo - rBuf
accSReady := true
if close <= rHi
upUsed := false
if close >= rLo
dnUsed := false
// 5a. pending setup transitions, highest priority first
if ambiguous
if ss != 0
ss := 0
pBar := -1
pExp := -1
else if ss == 1
exLow := outLo
if close > rHi
ss := 0
else if close >= rLo and close <= rHi and bar_index - pBar <= iReclaimWin
rcHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 2
else if bar_index - pBar > iReclaimWin
ss := 0
else if ss == 2
if low < exLow
ss := 0
else if close > rHi
ss := 0
else if close > rcHigh and close >= rLo and bar_index <= pExp
emit := 1
else if bar_index > pExp
ss := 0
else if ss == 3
exHigh := outHi
if close < rLo
ss := 0
else if close >= rLo and close <= rHi and bar_index - pBar <= iReclaimWin
rcLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 4
else if bar_index - pBar > iReclaimWin
ss := 0
else if ss == 4
if high > exHigh
ss := 0
else if close < rLo
ss := 0
else if close < rcLow and close <= rHi and bar_index <= pExp
emit := 2
else if bar_index > pExp
ss := 0
else if ss == 5
if close < rHi
ss := 0
failUp := close >= rLo
else if low <= rHi + rTol and close >= rHi and bar_index <= pExp
rcHigh := high
exLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 6
else if bar_index > pExp
ss := 0
else if ss == 6
exLow := math.min(exLow, low)
if close < rHi
ss := 0
failUp := close >= rLo
else if close > rcHigh and bar_index <= pExp
emit := 3
else if bar_index > pExp
ss := 0
else if ss == 7
if close > rLo
ss := 0
failDn := close <= rHi
else if high >= rLo - rTol and close <= rLo and bar_index <= pExp
rcLow := low
exHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 8
else if bar_index > pExp
ss := 0
else if ss == 8
exHigh := math.max(exHigh, high)
if close > rLo
ss := 0
failDn := close <= rHi
else if close < rcLow and bar_index <= pExp
emit := 4
else if bar_index > pExp
ss := 0
// 5b. a completed setup is measured, drawn and alerted once, here.
// It retires the range, including a structural failure.
if emit != 0
bool isLong = emit == 1 or emit == 3
bool isAcc = emit >= 3
string kind = (isLong ? "Bull " : "Bear ") + (isAcc ? "acceptance" : "rejection")
float entryPx = math.round_to_mintick(close)
float stopPx = math.round_to_mintick(isLong ? exLow - iStopTicks * mintick : exHigh + iStopTicks * mintick)
float targetPx = math.round_to_mintick(isAcc ? nearestPivot(close, isLong) : (isLong ? rHi : rLo))
[ok, risk, rew, rm, whyBad] = calcRisk(entryPx, stopPx, targetPx, isLong)
nowQual := ok and rm >= iMinR
lastSigBar := bar_index
lastSigKind := str.lower(kind)
lastSigR := rm
lastSigQual := nowQual
lastSigWhy := ok and not nowQual ? "below minimum R" : whyBad
if ok and (nowQual or not iScoreQual)
array.push(opEntry, entryPx)
array.push(opStop, stopPx)
array.push(opTarget, targetPx)
array.push(opRisk, risk)
array.push(opPlanR, rm)
array.push(opLong, isLong)
array.push(opAcc, isAcc)
if iShowLines and ok
int levelEnd = bar_index + iLevelBars
line.new(bar_index, entryPx, levelEnd, entryPx, color = C_ENTRY, width = iLevelWidth)
line.new(bar_index, stopPx, levelEnd, stopPx, color = C_STOP, width = iLevelWidth)
line.new(bar_index, targetPx, levelEnd, targetPx, color = C_TARGET, width = iLevelWidth)
label.new(levelEnd, entryPx, "Entry " + fmtPrice(entryPx), style = label.style_label_left, color = C_PANEL, textcolor = C_ENTRY, size = txtSize)
label.new(levelEnd, stopPx, "Stop " + fmtPrice(stopPx), style = label.style_label_left, color = C_PANEL, textcolor = C_STOP, size = txtSize)
label.new(levelEnd, targetPx, "Target " + fmtPrice(targetPx), style = label.style_label_left, color = C_PANEL, textcolor = C_TARGET, size = txtSize)
if iProject and isAcc and risk > 0
float dir = isLong ? 1.0 : -1.0
line.new(bar_index, entryPx + dir * 2 * risk, bar_index + 12, entryPx + dir * 2 * risk, color = color.new(C_TARGET, 60), style = line.style_dashed)
line.new(bar_index, entryPx + dir * 3 * risk, bar_index + 12, entryPx + dir * 3 * risk, color = color.new(C_TARGET, 60), style = line.style_dashed)
label.new(bar_index + 12, entryPx + dir * 2 * risk, "2R projection", style = label.style_label_left, color = C_PANEL, textcolor = C_TEXT, size = txtSize)
label.new(bar_index + 12, entryPx + dir * 3 * risk, "3R projection", style = label.style_label_left, color = C_PANEL, textcolor = C_TEXT, size = txtSize)
if iShowLbl
label.new(bar_index, isLong ? low - 0.6 * rAtr : high + 0.6 * rAtr,
setupText(kind, entryPx, stopPx, targetPx, rm, ok, whyBad, iShowLines) + (ok and not nowQual ? "\nbelow minimum R" : ""),
style = isLong ? label.style_label_up : label.style_label_down,
color = C_PANEL, textcolor = isLong ? C_BULL : C_BEAR, size = txtSize)
if nowQual or not iAlertQual
alert(kind + " | " + syminfo.ticker + " " + timeframe.period +
" | entry " + fmtPrice(entryPx) + " | stop " + fmtPrice(stopPx) +
" | target " + fmtPrice(targetPx) + " | " + fmtR(rm) +
" | " + (nowQual ? "qualified" : lastSigWhy),
alert.freq_once_per_bar_close)
ss := 0
signalled := true
rstate := 0
lastRetire := bar_index
lastRetireWhy := "signal complete"
// 5c. a close back inside after sustained closes beyond is the failed
// breakout this indicator exists to mark. It arms the opposite
// rejection whether or not the failing bar pokes through on its
// own, with the whole excursion behind the stop.
if failUp
ss := 4
exHigh := outHi
rcLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
else if failDn
ss := 2
exLow := outLo
rcHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
// 5d. acceptance arming outranks rejection initiation. Both closes
// must fall after the activation bar, and an ambiguous bar cannot arm.
if not signalled and not ambiguous and bar_index - rAct >= 2
if accBReady and close > rHi + rBuf and close[1] > rHi + rBuf and ss != 5 and ss != 6
ss := 5
pBar := bar_index
pExp := bar_index + iRetestWin
accBReady := false
upUsed := true
else if accSReady and close < rLo - rBuf and close[1] < rLo - rBuf and ss != 7 and ss != 8
ss := 7
pBar := bar_index
pExp := bar_index + iRetestWin
accSReady := false
dnUsed := true
// 5e. rejection initiation, only when flat and unambiguous. A same-bar
// reclaim is always a new event; a WAIT phase is not.
if ss == 0 and not signalled and not ambiguous
if breachDn and close >= rLo and close <= rHi
ss := 2
exLow := outLo
rcHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
else if breachDn and close < rLo and not dnUsed
ss := 1
exLow := outLo
dnUsed := true
pBar := bar_index
else if breachUp and close >= rLo and close <= rHi
ss := 4
exHigh := outHi
rcLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
else if breachUp and close > rHi and not upUsed
ss := 3
exHigh := outHi
upUsed := true
pBar := bar_index
// 6. a retired range fades, so the live range is the one that stands out
if wasLive and rstate == 0
color faded = color.new(iRangeColor, 65)
line.set_color(hiLine, faded)
line.set_color(loLine, faded)
line.set_width(hiLine, 1)
line.set_width(loLine, 1)
box.set_bgcolor(rangeBox, color.new(iRangeColor, 96))
if not na(hiLabel)
label.set_textcolor(hiLabel, faded)
label.set_textcolor(loLabel, faded)
//===================================================================== plots
// Qualified signals are triangles, unqualified signals are small muted circles.
// The marker is always printed: the minimum-R filter never erases a setup.
plotshape(emit == 1 and nowQual, "Bull rejection (qualified)", shape.triangleup, location.belowbar, C_BULL, text = "R", textcolor = C_BULL, size = size.normal)
plotshape(emit == 1 and not nowQual, "Bull rejection (below min R)", shape.circle, location.belowbar, color.new(C_MUTED, 30), text = "R", textcolor = C_MUTED, size = size.small)
plotshape(emit == 2 and nowQual, "Bear rejection (qualified)", shape.triangledown, location.abovebar, C_BEAR, text = "R", textcolor = C_BEAR, size = size.normal)
plotshape(emit == 2 and not nowQual, "Bear rejection (below min R)", shape.circle, location.abovebar, color.new(C_MUTED, 30), text = "R", textcolor = C_MUTED, size = size.small)
plotshape(emit == 3 and nowQual, "Bull acceptance (qualified)", shape.triangleup, location.belowbar, C_BULL, text = "A", textcolor = C_BULL, size = size.normal)
plotshape(emit == 3 and not nowQual, "Bull acceptance (below min R)", shape.circle, location.belowbar, color.new(C_MUTED, 30), text = "A", textcolor = C_MUTED, size = size.small)
plotshape(emit == 4 and nowQual, "Bear acceptance (qualified)", shape.triangledown, location.abovebar, C_BEAR, text = "A", textcolor = C_BEAR, size = size.normal)
plotshape(emit == 4 and not nowQual, "Bear acceptance (below min R)", shape.circle, location.abovebar, color.new(C_MUTED, 30), text = "A", textcolor = C_MUTED, size = size.small)
//==================================================================== alerts
bool gate = not iAlertQual or nowQual
alertcondition(emit == 1 and gate, "Bullish rejection", "Bullish rejection | {{ticker}} | {{interval}} | close {{close}}")
alertcondition(emit == 2 and gate, "Bearish rejection", "Bearish rejection | {{ticker}} | {{interval}} | close {{close}}")
alertcondition(emit == 3 and gate, "Bullish acceptance", "Bullish acceptance | {{ticker}} | {{interval}} | close {{close}}")
alertcondition(emit == 4 and gate, "Bearish acceptance", "Bearish acceptance | {{ticker}} | {{interval}} | close {{close}}")
//================================================================= dashboard
if iShowDash and barstate.islast
if na(dash)
dash := table.new(position.top_right, 2, 10, bgcolor = C_BG,
frame_color = color.new(C_RANGE, 60), frame_width = 1,
border_color = color.new(C_RANGE, 70), border_width = 1)
string stat = "seeking"
if rstate == 1
stat := "active"
else if iMode == "Manual"
stat := manualDone ? "spent, set new levels" : "waiting for activation"
else if lastRetire > 0 and bar_index - lastRetire <= iN
stat := "cooldown, " + lastRetireWhy
string rng = "none"
if rstate == 1
rng := fmtPrice(rLo) + " - " + fmtPrice(rHi)
else if iMode == "Manual" and iManHi <= iManLo
rng := "manual levels invalid"
string pend = "bear acc, awaiting close"
if ss == 0
pend := "none"
else if ss == 1
pend := "bull rej, awaiting reclaim"
else if ss == 2
pend := "bull rej, awaiting close"
else if ss == 3
pend := "bear rej, awaiting reclaim"
else if ss == 4
pend := "bear rej, awaiting close"
else if ss == 5
pend := "bull acc, awaiting retest"
else if ss == 6
pend := "bull acc, awaiting close"
else if ss == 7
pend := "bear acc, awaiting retest"
string sig = "none"
string note = "-"
if lastSigBar >= 0
sig := lastSigKind + " " + fmtR(lastSigR)
note := lastSigQual ? "qualified" : lastSigWhy
table.cell(dash, 0, 0, "Konatus RAR", text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 1, 0, iMode, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 1, "state", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 1, stat, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 2, "range", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 2, rng, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 3, "frozen ATR", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 3, rstate != 1 ? "-" : str.tostring(rAtr, format.mintick), text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 4, "pending", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 4, pend, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 5, "last signal", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 5, sig, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 6, "note", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 6, note, text_color = C_TEXT, text_size = txtSize)
if iShowScore
string scope = (iScoreQual ? "qualified" : "all valid") + ", no costs, " + str.tostring(array.size(opStop)) + " open"
table.cell(dash, 0, 7, "rejection score", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 7, fmtScore(rejWins, rejLoss, rejSumR), text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 8, "acceptance score", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 8, fmtScore(accWins, accLoss, accSumR), text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 9, "score scope", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 9, scope, text_color = C_TEXT, text_size = txtSize)
The download includes the Python model, all tests, the study script and its results, but not the price data. R multiples describe structure relative to a stop and are not expected returns. Read the trading and research disclaimer ↓
Bonus code · Konatus Spectrum + RAR 0.2.0 · Research prototype
This bonus script runs Konatus Spectrum and Konatus RAR together. Spectrum describes the trend across 240 moving averages. RAR marks what price did at a specific range. Each answers a question the other cannot, and neither overrides the other.
Read this first. A test written before any result was seen found that Spectrum’s trend did not improve RAR’s outcomes. The two work together as context and event on one chart. That is not evidence of better trades. TradingView’s translator accepts this exact source with zero errors and zero warnings, but it has not been run on a live chart. It is a large script, and only a real compile can confirm it stays within TradingView’s size and execution-time limits. If TradingView rejects it, run the two indicators side by side.
No. The test asked whether signals with Spectrum’s trend beat signals against it, across 2,175 signals on the same real-price data as above, before costs.
| Spectrum grade | Resolved | Reached target | Mean R per setup |
|---|---|---|---|
| With trend (R+, A+) | 520 | 59.8% | −0.057R (−0.136 to +0.023) |
| No trend (R, A) | 1,006 | 63.6% | −0.058R (−0.108 to −0.007) |
| Against trend (R−, A−) | 649 | 60.4% | −0.058R (−0.142 to +0.025) |
The difference between with-trend and against-trend signals was +0.001R, with an interval from −0.115 to +0.119. In the most recent 40% of the data it was −0.057R. So the script hides nothing by default. Narrowing alerts to with-trend signals would discard signals with no shown benefit, though the option is there if you want fewer alerts. The dashboard scores each grade separately, so you can run the same comparison on your own charts.
Install it the same way as above. It needs about 733 bars at the Balanced setting before Spectrum can grade anything; until then signals still print and read warming up. Use it instead of the two separate indicators, not alongside them. Alerts for Spectrum events need Any alert() function call.
//@version=6
// Konatus Spectrum + RAR 0.2.0 | kuttystauder.com
// Generated by build_combined.py from Konatus Spectrum and Konatus RAR 0.3.0.
//
// Two jobs on one chart. The ribbon describes the trend. RAR marks what price did
// at a range. Spectrum grades each RAR signal and never decides it: the same signals,
// stops and targets print as in Konatus RAR, every one marked.
//
// A test written before its results were seen found that the grade did not separate
// outcomes (With and Against both about -0.06R per signal before costs, on about
// 2,200 signals). The grade is context, not an edge. Scores are not probabilities.
//
// Plot budget: 20 plots x 2 counts + 19 series fills + 4 alertconditions = 63 of 64.
// Research prototype: not yet run on a live TradingView chart.
indicator("Konatus Spectrum + RAR", shorttitle="KS + RAR", overlay=true, max_bars_back=1500,
max_boxes_count=100, max_lines_count=200, max_labels_count=500)
string comboGroup = "Combination"
bool iRecede = input.bool(true, "Ribbon recedes while a range is live", group=comboGroup, tooltip="Dims the ribbon so the range and its levels read first. Display only.")
string iAlertGrade = input.string("All grades", "RAR alerts by Spectrum grade", options=["All grades", "Exclude against trend", "With trend only"], group=comboGroup, tooltip="Markers are never hidden. The published test found no outcome difference between grades, so narrowing this has no shown benefit.")
bool iSpectrumAlerts = input.bool(true, "Spectrum events in the alert() stream", group=comboGroup, tooltip="Trend established, recovery and fracturing, delivered through Any alert() function call. Spectrum's four alert conditions would exceed the plot budget.")
string engineGroup = "Adaptive engine"
string preset = input.string("Balanced", "Intraday sensitivity", options=["Fast", "Balanced", "Patient"], group=engineGroup)
float adaptiveShare = input.float(0.65, "Adaptive weight", minval=0.10, maxval=0.90, step=0.05, group=engineGroup, tooltip="Split between KAMA and FRAMA. Remaining weight is split between Hull and EMA. Higher is not necessarily better.")
int slopeBars = input.int(3, "Slope lookback", minval=1, maxval=10, group=engineGroup)
float slopeFloor = input.float(0.01, "Neutral slope (ATR per bar)", minval=0.0, maxval=0.10, step=0.005, group=engineGroup)
int confirmationBars = input.int(2, "Closed bars to confirm / invalidate", minval=1, maxval=5, group=engineGroup)
int recoveryBars = input.int(2, "Supportive closes for recovery", minval=1, maxval=5, group=engineGroup)
float pullbackDeadband = input.float(0.10, "Pullback momentum deadband", minval=0.01, maxval=0.50, step=0.01, group=engineGroup, tooltip="Eight-bar price change divided by ATR × sqrt(8). A pullback needs opposing momentum beyond this threshold or opposing fast slopes. Recovery needs supportive momentum and full trend evidence.")
string visualGroup = "Appearance"
bool showFill = input.bool(true, "Fill between strands", group=visualGroup)
int ribbonWidth = input.int(2, "Strand thickness", minval=1, maxval=3, group=visualGroup)
string G_RANGE = "1. Range identification"
string G_SETUP = "2. Setup windows"
string G_RISK = "3. Risk and target"
string G_DISP = "4. Display"
string G_ALERT = "5. Alerts"
iMode = input.string("Automatic", "Mode", options = ["Automatic", "Manual"], group = G_RANGE,
tooltip = "Automatic derives the range from recent price. Manual uses the levels below, from a separate profile study.")
iN = input.int(20, "Formation window (bars)", minval = 5, maxval = 200, group = G_RANGE,
tooltip = "Initial research parameter. Number of completed candles whose highest high and lowest low define the range.")
iAtrLen = input.int(14, "ATR length", minval = 2, maxval = 100, group = G_RANGE)
iMaxWidth = input.float(3.0, "Max range width (x ATR)", minval = 0.5, maxval = 10.0, step = 0.1, group = G_RANGE,
tooltip = "Initial research parameter. Rejects a formation window wider than this multiple of ATR.")
iTolAtr = input.float(0.15, "Boundary touch tolerance (x ATR)", minval = 0.01, maxval = 1.0, step = 0.01, group = G_RANGE,
tooltip = "Initial research parameter. A touch is a high or low within this distance of the boundary.")
iTouches = input.int(2, "Required touches per boundary", minval = 1, maxval = 10, group = G_RANGE)
iTouchGap = input.int(3, "Minimum bars between touches", minval = 1, maxval = 20, group = G_RANGE)
iRangeLife = input.int(100, "Automatic range life (bars)", minval = 10, maxval = 2000, group = G_RANGE,
tooltip = "Initial research parameter. Age at which an automatic range retires. Manual ranges do not age out.")
iManHi = input.float(0.0, "Manual upper boundary", group = G_RANGE)
iManLo = input.float(0.0, "Manual lower boundary", group = G_RANGE)
iManTs = input.time(timestamp("01 Jan 2024 00:00 +0000"), "Manual activation time", group = G_RANGE,
tooltip = "Manual levels are inert on every bar before this timestamp.")
iBufAtr = input.float(0.10, "Breakout buffer (x ATR)", minval = 0.0, maxval = 2.0, step = 0.01, group = G_SETUP,
tooltip = "Initial research parameter. Effective buffer is the larger of one tick and this multiple of frozen ATR.")
iReclaimWin = input.int(3, "Reclaim window after breach (bars)", minval = 0, maxval = 10, group = G_SETUP,
tooltip = "A breach may reclaim on the same candle or within this many following candles.")
iConfirmWin = input.int(5, "Confirmation window (bars)", minval = 1, maxval = 20, group = G_SETUP,
tooltip = "Bars allowed after the reclaim or retest candle for the confirmation close.")
iRetestWin = input.int(5, "Retest window after acceptance (bars)", minval = 1, maxval = 20, group = G_SETUP)
iPivL = input.int(3, "Pivot left bars", minval = 1, maxval = 50, group = G_RISK,
tooltip = "Confirmed swing definition. A pivot only becomes eligible after its right-side bars have printed.")
iPivR = input.int(3, "Pivot right bars", minval = 1, maxval = 50, group = G_RISK)
iStopTicks = input.int(2, "Stop buffer (ticks)", minval = 0, maxval = 100, group = G_RISK)
iMinR = input.float(2.0, "Minimum R to qualify", minval = 0.1, maxval = 20.0, step = 0.1, group = G_RISK,
tooltip = "Initial research parameter. Signals below this multiple are still marked, in a muted style.")
iProject = input.bool(false, "Draw 2R and 3R projection lines", group = G_RISK,
tooltip = "Projections are labelled as projections and are never used as the structural target.")
iRangeColor = input.color(#FFB300, "Range line color", group = G_DISP)
iRangeWidth = input.int(3, "Range line thickness", minval = 1, maxval = 5, group = G_DISP)
iLevelWidth = input.int(2, "Entry, stop and target thickness", minval = 1, maxval = 5, group = G_DISP)
iLevelBars = input.int(15, "Entry, stop and target length (bars)", minval = 5, maxval = 100, group = G_DISP)
iTextSize = input.string("Small", "Text size", options = ["Small", "Normal", "Large"], group = G_DISP)
iShowRangeLbl = input.bool(true, "Range high and low price labels", group = G_DISP)
iShowLbl = input.bool(true, "Signal labels", group = G_DISP)
iShowLines = input.bool(true, "Entry, stop and target lines", group = G_DISP)
iShowDash = input.bool(true, "Dashboard", group = G_DISP)
iShowMid = input.bool(true, "Range midpoint", group = G_DISP)
iShowScore = input.bool(true, "Scoreboard rows in the dashboard", group = G_DISP,
tooltip = "First touch of stop or target for each valid signal on this chart. Hypothetical fills at the signal close, no costs. A bar touching both counts as the stop.")
iScoreQual = input.bool(false, "Scoreboard counts qualified signals only", group = G_DISP)
string txtSize = iTextSize == "Large" ? size.large : iTextSize == "Normal" ? size.normal : size.small
iAlertQual = input.bool(false, "Alert only when minimum R is met", group = G_ALERT,
tooltip = "Applies to the four alert conditions and to the Any alert() function call stream.")
int startLength = preset == "Fast" ? 4 : preset == "Patient" ? 12 : 8
int stride = preset == "Fast" ? 2 : preset == "Patient" ? 6 : 4
int longestLength = startLength + stride * 59
float atr = ta.atr(14)
float unit = math.max(nz(atr, syminfo.mintick), syminfo.mintick)
f_clip(float value, float lo, float hi) =>
math.max(lo, math.min(hi, value))
f_efficiency(simple int n) =>
float travel = math.sum(math.abs(ta.change(close)), n)
not na(close[n]) and travel > 0 ? f_clip(math.abs(close - close[n]) / travel, 0.0, 1.0) : 0.0
f_vote(float value, float scale) =>
float delta = value - value[slopeBars]
float threshold = scale * slopeFloor * slopeBars
na(delta) ? 0.0 : delta > threshold ? 1.0 : delta < -threshold ? -1.0 : 0.0
// Each written call owns separate history. Do not move f_bank into a loop.
f_bank(simple int n, float scale) =>
float er = f_efficiency(n)
float ka = math.pow(er * (2.0 / 3.0 - 2.0 / 31.0) + 2.0 / 31.0, 2)
var float kama = na
kama := na(kama[1]) ? close : kama[1] + ka * (close - kama[1])
int half = int(n / 2)
float recentHigh = ta.highest(high, half)
float recentLow = ta.lowest(low, half)
float recentRange = recentHigh - recentLow
float olderRange = recentHigh[half] - recentLow[half]
float wholeRange = ta.highest(high, n) - ta.lowest(low, n)
float fa = 0.01
if bar_index >= n - 1 and wholeRange > 0
float rangeSum = recentRange + olderRange
float dimension = rangeSum > 0 ? math.log(2.0 * rangeSum / wholeRange) / math.log(2.0) : 1.0
fa := f_clip(math.exp(-4.6 * (dimension - 1.0)), 0.01, 1.0)
var float frama = na
frama := na(frama[1]) ? close : frama[1] + fa * (close - frama[1])
// Explicit integer convention: even half length, rounded square-root length.
float hull = ta.wma(2.0 * ta.wma(close, half) - ta.wma(close, n), int(math.round(math.sqrt(n))))
float ema = ta.ema(close, n)
float blend = adaptiveShare * (kama + frama) / 2.0 + (1.0 - adaptiveShare) * (hull + ema) / 2.0
float votes = adaptiveShare * (f_vote(kama, scale) + f_vote(frama, scale)) / 2.0 + (1.0 - adaptiveShare) * (f_vote(hull, scale) + f_vote(ema, scale)) / 2.0
[blend, votes]
// BEGIN GENERATED HORIZONS
[b00, v00] = f_bank(startLength + stride * 0, unit)
[b01, v01] = f_bank(startLength + stride * 1, unit)
[b02, v02] = f_bank(startLength + stride * 2, unit)
[b03, v03] = f_bank(startLength + stride * 3, unit)
[b04, v04] = f_bank(startLength + stride * 4, unit)
[b05, v05] = f_bank(startLength + stride * 5, unit)
[b06, v06] = f_bank(startLength + stride * 6, unit)
[b07, v07] = f_bank(startLength + stride * 7, unit)
[b08, v08] = f_bank(startLength + stride * 8, unit)
[b09, v09] = f_bank(startLength + stride * 9, unit)
[b10, v10] = f_bank(startLength + stride * 10, unit)
[b11, v11] = f_bank(startLength + stride * 11, unit)
[b12, v12] = f_bank(startLength + stride * 12, unit)
[b13, v13] = f_bank(startLength + stride * 13, unit)
[b14, v14] = f_bank(startLength + stride * 14, unit)
[b15, v15] = f_bank(startLength + stride * 15, unit)
[b16, v16] = f_bank(startLength + stride * 16, unit)
[b17, v17] = f_bank(startLength + stride * 17, unit)
[b18, v18] = f_bank(startLength + stride * 18, unit)
[b19, v19] = f_bank(startLength + stride * 19, unit)
[b20, v20] = f_bank(startLength + stride * 20, unit)
[b21, v21] = f_bank(startLength + stride * 21, unit)
[b22, v22] = f_bank(startLength + stride * 22, unit)
[b23, v23] = f_bank(startLength + stride * 23, unit)
[b24, v24] = f_bank(startLength + stride * 24, unit)
[b25, v25] = f_bank(startLength + stride * 25, unit)
[b26, v26] = f_bank(startLength + stride * 26, unit)
[b27, v27] = f_bank(startLength + stride * 27, unit)
[b28, v28] = f_bank(startLength + stride * 28, unit)
[b29, v29] = f_bank(startLength + stride * 29, unit)
[b30, v30] = f_bank(startLength + stride * 30, unit)
[b31, v31] = f_bank(startLength + stride * 31, unit)
[b32, v32] = f_bank(startLength + stride * 32, unit)
[b33, v33] = f_bank(startLength + stride * 33, unit)
[b34, v34] = f_bank(startLength + stride * 34, unit)
[b35, v35] = f_bank(startLength + stride * 35, unit)
[b36, v36] = f_bank(startLength + stride * 36, unit)
[b37, v37] = f_bank(startLength + stride * 37, unit)
[b38, v38] = f_bank(startLength + stride * 38, unit)
[b39, v39] = f_bank(startLength + stride * 39, unit)
[b40, v40] = f_bank(startLength + stride * 40, unit)
[b41, v41] = f_bank(startLength + stride * 41, unit)
[b42, v42] = f_bank(startLength + stride * 42, unit)
[b43, v43] = f_bank(startLength + stride * 43, unit)
[b44, v44] = f_bank(startLength + stride * 44, unit)
[b45, v45] = f_bank(startLength + stride * 45, unit)
[b46, v46] = f_bank(startLength + stride * 46, unit)
[b47, v47] = f_bank(startLength + stride * 47, unit)
[b48, v48] = f_bank(startLength + stride * 48, unit)
[b49, v49] = f_bank(startLength + stride * 49, unit)
[b50, v50] = f_bank(startLength + stride * 50, unit)
[b51, v51] = f_bank(startLength + stride * 51, unit)
[b52, v52] = f_bank(startLength + stride * 52, unit)
[b53, v53] = f_bank(startLength + stride * 53, unit)
[b54, v54] = f_bank(startLength + stride * 54, unit)
[b55, v55] = f_bank(startLength + stride * 55, unit)
[b56, v56] = f_bank(startLength + stride * 56, unit)
[b57, v57] = f_bank(startLength + stride * 57, unit)
[b58, v58] = f_bank(startLength + stride * 58, unit)
[b59, v59] = f_bank(startLength + stride * 59, unit)
float s00 = (b00 + b01 + b02) / 3.0
float a00 = (v00 + v01 + v02) / 3.0
float s01 = (b03 + b04 + b05) / 3.0
float a01 = (v03 + v04 + v05) / 3.0
float s02 = (b06 + b07 + b08) / 3.0
float a02 = (v06 + v07 + v08) / 3.0
float s03 = (b09 + b10 + b11) / 3.0
float a03 = (v09 + v10 + v11) / 3.0
float s04 = (b12 + b13 + b14) / 3.0
float a04 = (v12 + v13 + v14) / 3.0
float s05 = (b15 + b16 + b17) / 3.0
float a05 = (v15 + v16 + v17) / 3.0
float s06 = (b18 + b19 + b20) / 3.0
float a06 = (v18 + v19 + v20) / 3.0
float s07 = (b21 + b22 + b23) / 3.0
float a07 = (v21 + v22 + v23) / 3.0
float s08 = (b24 + b25 + b26) / 3.0
float a08 = (v24 + v25 + v26) / 3.0
float s09 = (b27 + b28 + b29) / 3.0
float a09 = (v27 + v28 + v29) / 3.0
float s10 = (b30 + b31 + b32) / 3.0
float a10 = (v30 + v31 + v32) / 3.0
float s11 = (b33 + b34 + b35) / 3.0
float a11 = (v33 + v34 + v35) / 3.0
float s12 = (b36 + b37 + b38) / 3.0
float a12 = (v36 + v37 + v38) / 3.0
float s13 = (b39 + b40 + b41) / 3.0
float a13 = (v39 + v40 + v41) / 3.0
float s14 = (b42 + b43 + b44) / 3.0
float a14 = (v42 + v43 + v44) / 3.0
float s15 = (b45 + b46 + b47) / 3.0
float a15 = (v45 + v46 + v47) / 3.0
float s16 = (b48 + b49 + b50) / 3.0
float a16 = (v48 + v49 + v50) / 3.0
float s17 = (b51 + b52 + b53) / 3.0
float a17 = (v51 + v52 + v53) / 3.0
float s18 = (b54 + b55 + b56) / 3.0
float a18 = (v54 + v55 + v56) / 3.0
float s19 = (b57 + b58 + b59) / 3.0
float a19 = (v57 + v58 + v59) / 3.0
array<float> strands = array.from(s00, s01, s02, s03, s04, s05, s06, s07, s08, s09, s10, s11, s12, s13, s14, s15, s16, s17, s18, s19)
array<float> votes = array.from(a00, a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)
// END GENERATED HORIZONS
float broadVote = array.avg(votes)
float fastVote = 0.0
float slowVote = 0.0
float orderVote = 0.0
for i = 0 to 19
if i < 7
fastVote += array.get(votes, i) / 7.0
if i >= 13
slowVote += array.get(votes, i) / 7.0
if i < 19
float spacing = array.get(strands, i) - array.get(strands, i + 1)
orderVote += (spacing > unit * 0.01 ? 1.0 : spacing < -unit * 0.01 ? -1.0 : 0.0) / 19.0
f_roc(simple int n) =>
not na(close[n]) and close[n] != 0 ? 100.0 * (close - close[n]) / math.abs(close[n]) : 0.0
float roc8 = f_roc(8)
float roc21 = f_roc(21)
float roc55 = f_roc(55)
float fastMove = (close - close[8]) / (unit * math.sqrt(8.0))
float atrPercent = 100.0 * unit / math.max(math.abs(close), syminfo.mintick)
float momentum = (f_clip(roc8 / (atrPercent * math.sqrt(8.0)), -1.0, 1.0) + f_clip(roc21 / (atrPercent * math.sqrt(21.0)), -1.0, 1.0) + f_clip(roc55 / (atrPercent * math.sqrt(55.0)), -1.0, 1.0)) / 3.0
float efficiency = f_efficiency(21)
float spread = (array.max(strands) - array.min(strands)) / unit
float meanSpread = ta.sma(spread, 100)
bool compressed = spread < 0.40 or (not na(meanSpread) and spread < meanSpread * 0.65)
float smoothSpread = ta.ema(spread, 5)
float spreadChange = (smoothSpread - smoothSpread[3]) / 3.0
string widthMotion = spreadChange > 0.01 ? "Expanding" : spreadChange < -0.01 ? "Contracting" : "Steady"
bool ready = bar_index >= longestLength * 3 and not na(s19) and not na(atr) and not na(meanSpread)
// State and dashboard evidence are committed only when the bar closes.
var int direction = 0
var int pending = 0
var int streak = 0
var int weakBars = 0
var bool inPullback = false
var int recoveryStreak = 0
var string state = "Warming up"
var string health = "Warming up"
var string closedStructure = "Warming up"
var float closedAgreement = na
var float closedMomentum = na
var float closedSpread = na
var float closedEfficiency = na
if barstate.isconfirmed
if not ready
direction := 0
pending := 0
streak := 0
weakBars := 0
inPullback := false
recoveryStreak := 0
state := "Warming up"
health := "Warming up"
closedStructure := "Warming up"
closedAgreement := na
closedMomentum := na
closedSpread := na
closedEfficiency := na
else
int previousDirection = direction
int candidate = 0
if slowVote >= 0.50 and broadVote >= 0.35 and orderVote >= 0.20 and momentum > 0.10 and efficiency >= 0.20
candidate := 1
else if slowVote <= -0.50 and broadVote <= -0.35 and orderVote <= -0.20 and momentum < -0.10 and efficiency >= 0.20
candidate := -1
streak := candidate != 0 and candidate == pending ? streak + 1 : candidate != 0 ? 1 : 0
pending := candidate
bool weak = direction != 0 and slowVote * direction < 0.15
weakBars := weak ? weakBars + 1 : 0
if weakBars >= confirmationBars
direction := 0
weakBars := 0
if candidate != 0 and streak >= confirmationBars
direction := candidate
weakBars := 0
if direction == 0 or direction != previousDirection
inPullback := false
recoveryStreak := 0
if direction == 0
health := "Unestablished"
else if slowVote * direction < 0.35
health := "Weakening"
else if compressed or efficiency < 0.20
health := "Stalled"
else
health := candidate == direction ? "Strong" : "Weakening"
if direction != 0
if slowVote * direction < 0.35
inPullback := false
recoveryStreak := 0
state := "Fracturing"
else
bool adverse = fastVote * direction < -0.10 or fastMove * direction < -pullbackDeadband
bool supportive = fastVote * direction >= 0.10 and fastMove * direction > pullbackDeadband and health == "Strong"
if adverse
inPullback := true
recoveryStreak := inPullback and supportive ? recoveryStreak + 1 : 0
if recoveryStreak >= recoveryBars
inPullback := false
recoveryStreak := 0
state := inPullback ? "Pullback" : health == "Strong" ? "Trending" : health
else
state := compressed ? "Compression" : math.abs(fastVote) >= 0.40 and fastVote * fastMove > 0 and efficiency >= 0.20 ? "Building" : "Mixed"
closedAgreement := broadVote * 100.0
closedMomentum := momentum * 100.0
closedSpread := spread
closedEfficiency := efficiency * 100.0
closedStructure := (compressed ? "Compressed · " : "") + widthMotion
bool eventGate = barstate.isconfirmed and ready and ready[1]
bool bullEvent = eventGate and direction == 1 and direction[1] != 1
bool bearEvent = eventGate and direction == -1 and direction[1] != -1
bool recoveryEvent = eventGate and direction != 0 and direction == direction[1] and state[1] == "Pullback" and state == "Trending"
bool fractureEvent = eventGate and state == "Fracturing" and state[1] != "Fracturing"
//=================================================================== palette
color C_RANGE = #787b86
color C_FILL = color.new(iRangeColor, 90)
color C_MID = color.new(iRangeColor, 30)
color C_BULL = #26a69a
color C_BEAR = #ef5350
color C_MUTED = #787b86
color C_ENTRY = #b2b5be
color C_STOP = #ef5350
color C_TARGET = #26a69a
color C_TEXT = #d1d4dc
color C_BG = color.new(#131722, 10)
color C_PANEL = color.new(#131722, 15)
//============================================================== global series
float mintick = syminfo.mintick
float rarAtr = ta.atr(iAtrLen)
float hhN = ta.highest(high, iN)[1]
float llN = ta.lowest(low, iN)[1]
float atrRef = rarAtr[1]
float ph = ta.pivothigh(high, iPivL, iPivR)
float pl = ta.pivotlow(low, iPivL, iPivR)
//=============================================================== state storage
// range state: 0 seeking, 1 active
var int rstate = 0
var float rHi = na
var float rLo = na
var float rAtr = na
var float rTol = na
var float rBuf = na
var int rAct = -1
var int lastRetire = -100000
var string lastRetireWhy = ""
// setup state: 0 none, 1 bull rejection awaiting reclaim, 2 bull rejection pending,
// 3 bear rejection awaiting reclaim, 4 bear rejection pending,
// 5 bull acceptance awaiting retest, 6 bull acceptance pending,
// 7 bear acceptance awaiting retest, 8 bear acceptance pending
var int ss = 0
var int pBar = -1
var int pExp = -1
var float exLow = na
var float exHigh = na
var float rcHigh = na
var float rcLow = na
var bool accBReady = true
var bool accSReady = true
var float outHi = na // highest high above the range in this excursion
var float outLo = na // lowest low below the range in this excursion
var bool upUsed = false // this excursion has already spent its reclaim window
var bool dnUsed = false
var bool signalled = false
var bool manualDone = false
// pivots confirmed on or before the current bar
var array<float> pivHiPrice = array.new<float>()
var array<int> pivHiKnown = array.new<int>()
var array<float> pivLoPrice = array.new<float>()
var array<int> pivLoKnown = array.new<int>()
// signals awaiting first touch of stop or target, and the running score
var array<float> opEntry = array.new<float>()
var array<float> opStop = array.new<float>()
var array<float> opTarget = array.new<float>()
var array<float> opRisk = array.new<float>()
var array<float> opPlanR = array.new<float>()
var array<bool> opLong = array.new<bool>()
var array<int> opGrade = array.new<int>()
var int withWins = 0
var int withLoss = 0
var float withSumR = 0.0
var int flatWins = 0
var int flatLoss = 0
var float flatSumR = 0.0
var int contraWins = 0
var int contraLoss = 0
var float contraSumR = 0.0
// drawings
var box rangeBox = na
var line midLine = na
var line hiLine = na
var line loLine = na
var label hiLabel = na
var label loLabel = na
var table dash = na
// per-bar signal: 0 none, 1 bull rejection, 2 bear rejection,
// 3 bull acceptance, 4 bear acceptance
int emit = 0
bool nowQual = false
bool gradePass = true
// last completed signal, for the dashboard
var int lastSigBar = -1
var string lastSigKind = "none"
var float lastSigR = na
var bool lastSigQual = false
var string lastSigWhy = ""
var string lastSigGrade = ""
//================================================================== utilities
fmtPrice(float p) =>
na(p) ? "n/a" : str.tostring(p, format.mintick)
calcRisk(float entry, float stop, float target, bool isLong) =>
float risk = math.abs(entry - stop)
bool ok = true
string why = ""
if na(target)
ok := false
why := "No structural target"
else if isLong and not (stop < entry and entry < target)
ok := false
why := "Invalid price ordering"
else if not isLong and not (stop > entry and entry > target)
ok := false
why := "Invalid price ordering"
else if risk <= 0
ok := false
why := "Zero risk"
float reward = ok ? math.abs(target - entry) : na
float rmult = ok ? reward / risk : na
[ok, risk, reward, rmult, why]
nearestPivot(float entry, bool isLong) =>
float best = na
if isLong
int n = array.size(pivHiPrice)
if n > 0
for k = 0 to n - 1
float p = array.get(pivHiPrice, k)
int known = array.get(pivHiKnown, k)
if known < bar_index and p > entry
best := na(best) ? p : math.min(best, p)
else
int m = array.size(pivLoPrice)
if m > 0
for k = 0 to m - 1
float p = array.get(pivLoPrice, k)
int known = array.get(pivLoKnown, k)
if known < bar_index and p < entry
best := na(best) ? p : math.max(best, p)
best
fmtR(float r) =>
na(r) ? "R n/a" : str.tostring(r, "#.##") + "R"
fmtScore(int wins, int losses, float sumR) =>
int n = wins + losses
n == 0 ? "no resolved signals" : str.tostring(wins) + " target, " + str.tostring(losses) + " stop, " + str.tostring(sumR / n, "+#.##;-#.##") + "R avg"
setupText(string kind, float entry, float stop, float target, float rmult, bool ok, string why, bool compact) =>
string s = kind
if not (compact and ok)
s := s + "\nentry " + fmtPrice(entry) + "\nstop " + fmtPrice(stop) + "\ntarget " + fmtPrice(target)
s := s + "\n" + fmtR(rmult)
if not ok
s := s + "\n" + why
s
//=================================================== confirmed-bar state machine
if barstate.isconfirmed
// 1. pivot bookkeeping. ta.pivothigh returns a value only on the bar that
// confirms the pivot, which is what keeps the target free of hindsight.
if not na(ph)
array.push(pivHiPrice, ph)
array.push(pivHiKnown, bar_index)
if array.size(pivHiPrice) > 100
array.shift(pivHiPrice)
array.shift(pivHiKnown)
if not na(pl)
array.push(pivLoPrice, pl)
array.push(pivLoKnown, bar_index)
if array.size(pivLoPrice) > 100
array.shift(pivLoPrice)
array.shift(pivLoKnown)
// 2. score earlier signals first, so a signal cannot score on its own bar.
// A bar touching both levels counts as the stop, because the order inside
// a bar is unknowable, and a gap through the stop fills at the open.
int nOpen = array.size(opStop)
if nOpen > 0
for k = nOpen - 1 to 0
bool tradeLong = array.get(opLong, k)
float tradeStop = array.get(opStop, k)
float tradeTgt = array.get(opTarget, k)
bool hitStop = tradeLong ? low <= tradeStop : high >= tradeStop
bool hitTgt = tradeLong ? high >= tradeTgt : low <= tradeTgt
if hitStop or hitTgt
float tradeEntry = array.get(opEntry, k)
float fillPx = tradeLong ? math.min(open, tradeStop) : math.max(open, tradeStop)
float moved = tradeLong ? fillPx - tradeEntry : tradeEntry - fillPx
float gotR = hitStop ? moved / array.get(opRisk, k) : array.get(opPlanR, k)
int tradeGrade = array.get(opGrade, k)
if tradeGrade > 0
withWins := withWins + (hitStop ? 0 : 1)
withLoss := withLoss + (hitStop ? 1 : 0)
withSumR := withSumR + gotR
else if tradeGrade < 0
contraWins := contraWins + (hitStop ? 0 : 1)
contraLoss := contraLoss + (hitStop ? 1 : 0)
contraSumR := contraSumR + gotR
else
flatWins := flatWins + (hitStop ? 0 : 1)
flatLoss := flatLoss + (hitStop ? 1 : 0)
flatSumR := flatSumR + gotR
array.remove(opEntry, k)
array.remove(opStop, k)
array.remove(opTarget, k)
array.remove(opRisk, k)
array.remove(opPlanR, k)
array.remove(opLong, k)
array.remove(opGrade, k)
// 3. range activation
if rstate == 0
float newHi = na
float newLo = na
float newAtr = na
int newLeft = bar_index
if iMode == "Automatic"
bool fresh = bar_index - lastRetire > iN
bool formReady = bar_index >= iN + iAtrLen and not na(atrRef) and atrRef > 0
if fresh and formReady and close >= llN and close <= hhN
float tolRef = iTolAtr * atrRef
int tHi = 0
int tLo = 0
int lastHi = -1
int lastLo = -1
for k = 1 to iN
int off = iN + 1 - k
if high[off] >= hhN - tolRef
if lastHi < 0 or math.abs(off - lastHi) >= iTouchGap
tHi := tHi + 1
lastHi := off
if low[off] <= llN + tolRef
if lastLo < 0 or math.abs(off - lastLo) >= iTouchGap
tLo := tLo + 1
lastLo := off
bool narrow = hhN - llN <= iMaxWidth * atrRef
bool both = tHi >= iTouches and tLo >= iTouches
if narrow and both and hhN > llN
newHi := hhN
newLo := llN
newAtr := atrRef
newLeft := bar_index - iN
else if not manualDone and iManHi > iManLo and not na(rarAtr) and rarAtr > 0 and time >= iManTs
manualDone := true
newHi := iManHi
newLo := iManLo
newAtr := rarAtr
if not na(newHi)
rHi := newHi
rLo := newLo
rAtr := newAtr
rTol := iTolAtr * newAtr
rBuf := math.max(mintick, iBufAtr * newAtr)
rAct := bar_index
rstate := 1
ss := 0
signalled := false
accBReady := true
accSReady := true
outHi := na
outLo := na
upUsed := false
dnUsed := false
rangeBox := box.new(newLeft, rHi, bar_index, rLo,
border_color = color.new(iRangeColor, 100), border_width = 0, bgcolor = C_FILL,
xloc = xloc.bar_index)
hiLine := line.new(newLeft, rHi, bar_index, rHi, color = iRangeColor, width = iRangeWidth, xloc = xloc.bar_index)
loLine := line.new(newLeft, rLo, bar_index, rLo, color = iRangeColor, width = iRangeWidth, xloc = xloc.bar_index)
hiLabel := na
loLabel := na
if iShowRangeLbl
hiLabel := label.new(bar_index, rHi, "Range high " + fmtPrice(rHi), style = label.style_label_left, color = C_PANEL, textcolor = iRangeColor, size = txtSize)
loLabel := label.new(bar_index, rLo, "Range low " + fmtPrice(rLo), style = label.style_label_left, color = C_PANEL, textcolor = iRangeColor, size = txtSize)
if iShowMid
midLine := line.new(newLeft, (rHi + rLo) / 2, bar_index,
(rHi + rLo) / 2, color = C_MID, style = line.style_dotted,
xloc = xloc.bar_index)
else
midLine := na
label.new(bar_index, rLo, iMode == "Automatic" ? "range known" : "levels active",
style = label.style_label_up, color = C_PANEL, textcolor = C_TEXT, size = txtSize)
// 4. extend the frozen drawings while the range is live, including the bar
// on which it retires
bool wasLive = rstate == 1
if rstate == 1 and not na(rangeBox)
box.set_right(rangeBox, bar_index)
line.set_x2(hiLine, bar_index)
line.set_x2(loLine, bar_index)
if not na(midLine)
line.set_x2(midLine, bar_index)
if not na(hiLabel)
label.set_x(hiLabel, bar_index)
label.set_x(loLabel, bar_index)
// 5. evaluation, only after the activation bar
if rstate == 1 and not na(rHi)
if iMode == "Automatic" and (bar_index - rAct) > iRangeLife
rstate := 0
ss := 0
pBar := -1
pExp := -1
lastRetire := bar_index
lastRetireWhy := "range life exceeded"
else if bar_index > rAct
bool breachUp = high > rHi + rBuf
bool breachDn = low < rLo - rBuf
bool ambiguous = breachUp and breachDn
bool failUp = false
bool failDn = false
// excursion tracking: from the first trade beyond a boundary until a
// bar after a close back inside, whichever setup branch is live, so
// every rejection stop can clear all of it
if close[1] < rHi
outHi := na
if close[1] > rLo
outLo := na
if high > rHi
outHi := na(outHi) ? high : math.max(outHi, high)
if low < rLo
outLo := na(outLo) ? low : math.min(outLo, low)
// latches: a fresh two-close sequence is required after price returns
// to the boundary, and one excursion gets one reclaim window
if close <= rHi + rBuf
accBReady := true
if close >= rLo - rBuf
accSReady := true
if close <= rHi
upUsed := false
if close >= rLo
dnUsed := false
// 5a. pending setup transitions, highest priority first
if ambiguous
if ss != 0
ss := 0
pBar := -1
pExp := -1
else if ss == 1
exLow := outLo
if close > rHi
ss := 0
else if close >= rLo and close <= rHi and bar_index - pBar <= iReclaimWin
rcHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 2
else if bar_index - pBar > iReclaimWin
ss := 0
else if ss == 2
if low < exLow
ss := 0
else if close > rHi
ss := 0
else if close > rcHigh and close >= rLo and bar_index <= pExp
emit := 1
else if bar_index > pExp
ss := 0
else if ss == 3
exHigh := outHi
if close < rLo
ss := 0
else if close >= rLo and close <= rHi and bar_index - pBar <= iReclaimWin
rcLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 4
else if bar_index - pBar > iReclaimWin
ss := 0
else if ss == 4
if high > exHigh
ss := 0
else if close < rLo
ss := 0
else if close < rcLow and close <= rHi and bar_index <= pExp
emit := 2
else if bar_index > pExp
ss := 0
else if ss == 5
if close < rHi
ss := 0
failUp := close >= rLo
else if low <= rHi + rTol and close >= rHi and bar_index <= pExp
rcHigh := high
exLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 6
else if bar_index > pExp
ss := 0
else if ss == 6
exLow := math.min(exLow, low)
if close < rHi
ss := 0
failUp := close >= rLo
else if close > rcHigh and bar_index <= pExp
emit := 3
else if bar_index > pExp
ss := 0
else if ss == 7
if close > rLo
ss := 0
failDn := close <= rHi
else if high >= rLo - rTol and close <= rLo and bar_index <= pExp
rcLow := low
exHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
ss := 8
else if bar_index > pExp
ss := 0
else if ss == 8
exHigh := math.max(exHigh, high)
if close > rLo
ss := 0
failDn := close <= rHi
else if close < rcLow and bar_index <= pExp
emit := 4
else if bar_index > pExp
ss := 0
// 5b. a completed setup is measured, drawn and alerted once, here.
// It retires the range, including a structural failure.
if emit != 0
bool isLong = emit == 1 or emit == 3
bool isAcc = emit >= 3
string kind = (isLong ? "Bull " : "Bear ") + (isAcc ? "acceptance" : "rejection")
float entryPx = math.round_to_mintick(close)
float stopPx = math.round_to_mintick(isLong ? exLow - iStopTicks * mintick : exHigh + iStopTicks * mintick)
float targetPx = math.round_to_mintick(isAcc ? nearestPivot(close, isLong) : (isLong ? rHi : rLo))
[ok, risk, rew, rm, whyBad] = calcRisk(entryPx, stopPx, targetPx, isLong)
nowQual := ok and rm >= iMinR
bool warming = state == "Warming up"
int sigGrade = direction == 0 ? 0 : direction == (isLong ? 1 : -1) ? 1 : -1
string gradeName = warming ? "warming up" : sigGrade > 0 ? "with trend" : sigGrade < 0 ? "against trend" : "no trend"
gradePass := iAlertGrade == "All grades" or warming or sigGrade > 0 or (iAlertGrade == "Exclude against trend" and sigGrade == 0)
lastSigGrade := gradeName
lastSigBar := bar_index
lastSigKind := str.lower(kind)
lastSigR := rm
lastSigQual := nowQual
lastSigWhy := ok and not nowQual ? "below minimum R" : whyBad
if ok and not warming and (nowQual or not iScoreQual)
array.push(opEntry, entryPx)
array.push(opStop, stopPx)
array.push(opTarget, targetPx)
array.push(opRisk, risk)
array.push(opPlanR, rm)
array.push(opLong, isLong)
array.push(opGrade, sigGrade)
if iShowLines and ok
int levelEnd = bar_index + iLevelBars
line.new(bar_index, entryPx, levelEnd, entryPx, color = C_ENTRY, width = iLevelWidth)
line.new(bar_index, stopPx, levelEnd, stopPx, color = C_STOP, width = iLevelWidth)
line.new(bar_index, targetPx, levelEnd, targetPx, color = C_TARGET, width = iLevelWidth)
label.new(levelEnd, entryPx, "Entry " + fmtPrice(entryPx), style = label.style_label_left, color = C_PANEL, textcolor = C_ENTRY, size = txtSize)
label.new(levelEnd, stopPx, "Stop " + fmtPrice(stopPx), style = label.style_label_left, color = C_PANEL, textcolor = C_STOP, size = txtSize)
label.new(levelEnd, targetPx, "Target " + fmtPrice(targetPx), style = label.style_label_left, color = C_PANEL, textcolor = C_TARGET, size = txtSize)
if iProject and isAcc and risk > 0
float dir = isLong ? 1.0 : -1.0
line.new(bar_index, entryPx + dir * 2 * risk, bar_index + 12, entryPx + dir * 2 * risk, color = color.new(C_TARGET, 60), style = line.style_dashed)
line.new(bar_index, entryPx + dir * 3 * risk, bar_index + 12, entryPx + dir * 3 * risk, color = color.new(C_TARGET, 60), style = line.style_dashed)
label.new(bar_index + 12, entryPx + dir * 2 * risk, "2R projection", style = label.style_label_left, color = C_PANEL, textcolor = C_TEXT, size = txtSize)
label.new(bar_index + 12, entryPx + dir * 3 * risk, "3R projection", style = label.style_label_left, color = C_PANEL, textcolor = C_TEXT, size = txtSize)
// Labels cost no plot counts, and the 64-count budget belongs to the ribbon.
// Every signal is marked. No filter hides one.
string mark = (isAcc ? "A" : "R") + (warming or sigGrade == 0 ? "" : sigGrade > 0 ? "+" : "-")
label.new(bar_index, close, mark, yloc = isLong ? yloc.belowbar : yloc.abovebar,
style = nowQual ? (isLong ? label.style_triangleup : label.style_triangledown) : label.style_circle,
color = nowQual ? (isLong ? C_BULL : C_BEAR) : color.new(C_MUTED, 30),
textcolor = nowQual ? (isLong ? C_BULL : C_BEAR) : C_TEXT, size = nowQual ? size.normal : size.small)
if iShowLbl
label.new(bar_index, isLong ? low - 0.6 * rAtr : high + 0.6 * rAtr,
setupText(kind, entryPx, stopPx, targetPx, rm, ok, whyBad, iShowLines) + (ok and not nowQual ? "\nbelow minimum R" : "") + "\nSpectrum: " + gradeName,
style = isLong ? label.style_label_up : label.style_label_down,
color = C_PANEL, textcolor = isLong ? C_BULL : C_BEAR, size = txtSize)
if (nowQual or not iAlertQual) and gradePass
alert(kind + " | " + syminfo.ticker + " " + timeframe.period +
" | entry " + fmtPrice(entryPx) + " | stop " + fmtPrice(stopPx) +
" | target " + fmtPrice(targetPx) + " | " + fmtR(rm) +
" | " + (nowQual ? "qualified" : lastSigWhy) + " | Spectrum: " + gradeName,
alert.freq_once_per_bar_close)
ss := 0
signalled := true
rstate := 0
lastRetire := bar_index
lastRetireWhy := "signal complete"
// 5c. a close back inside after sustained closes beyond is the failed
// breakout this indicator exists to mark. It arms the opposite
// rejection whether or not the failing bar pokes through on its
// own, with the whole excursion behind the stop.
if failUp
ss := 4
exHigh := outHi
rcLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
else if failDn
ss := 2
exLow := outLo
rcHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
// 5d. acceptance arming outranks rejection initiation. Both closes
// must fall after the activation bar, and an ambiguous bar cannot arm.
if not signalled and not ambiguous and bar_index - rAct >= 2
if accBReady and close > rHi + rBuf and close[1] > rHi + rBuf and ss != 5 and ss != 6
ss := 5
pBar := bar_index
pExp := bar_index + iRetestWin
accBReady := false
upUsed := true
else if accSReady and close < rLo - rBuf and close[1] < rLo - rBuf and ss != 7 and ss != 8
ss := 7
pBar := bar_index
pExp := bar_index + iRetestWin
accSReady := false
dnUsed := true
// 5e. rejection initiation, only when flat and unambiguous. A same-bar
// reclaim is always a new event; a WAIT phase is not.
if ss == 0 and not signalled and not ambiguous
if breachDn and close >= rLo and close <= rHi
ss := 2
exLow := outLo
rcHigh := high
pBar := bar_index
pExp := bar_index + iConfirmWin
else if breachDn and close < rLo and not dnUsed
ss := 1
exLow := outLo
dnUsed := true
pBar := bar_index
else if breachUp and close >= rLo and close <= rHi
ss := 4
exHigh := outHi
rcLow := low
pBar := bar_index
pExp := bar_index + iConfirmWin
else if breachUp and close > rHi and not upUsed
ss := 3
exHigh := outHi
upUsed := true
pBar := bar_index
// 6. a retired range fades, so the live range is the one that stands out
if wasLive and rstate == 0
color faded = color.new(iRangeColor, 65)
line.set_color(hiLine, faded)
line.set_color(loLine, faded)
line.set_width(hiLine, 1)
line.set_width(loLine, 1)
box.set_bgcolor(rangeBox, color.new(iRangeColor, 96))
if not na(hiLabel)
label.set_textcolor(hiLabel, faded)
label.set_textcolor(loLabel, faded)
//===================================================================== plots
// Hue identifies horizon. Agreement/momentum/efficiency control brightness.
f_hue(simple int index) =>
float x = float(index)
x <= 4 ? color.from_gradient(x, 0, 4, #FFD166, #FF7043) : x <= 8 ? color.from_gradient(x, 4, 8, #FF7043, #EF5DA8) : x <= 12 ? color.from_gradient(x, 8, 12, #EF5DA8, #A775FF) : x <= 16 ? color.from_gradient(x, 12, 16, #A775FF, #548CFF) : color.from_gradient(x, 16, 19, #548CFF, #4DE3D0)
f_tint(simple int index, float agreement, bool filling) =>
float support = math.max(0.0, math.sign(agreement) * momentum)
float quality = ready ? f_clip(0.60 * math.abs(agreement) + 0.25 * support + 0.15 * efficiency, 0.0, 1.0) : 0.0
color tint = color.from_gradient(quality, 0.0, 1.0, #737A90, f_hue(index))
float transparency = filling ? 92.0 - 24.0 * quality : 68.0 - 58.0 * quality
// The range and its levels come forward while a range is live.
color.new(tint, iRecede and rstate == 1 ? math.min(95.0, transparency + 18.0) : transparency)
// BEGIN GENERATED VISUALS
p00 = plot(s00, "Spectrum 01", color=f_tint(0, a00, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p01 = plot(s01, "Spectrum 02", color=f_tint(1, a01, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p02 = plot(s02, "Spectrum 03", color=f_tint(2, a02, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p03 = plot(s03, "Spectrum 04", color=f_tint(3, a03, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p04 = plot(s04, "Spectrum 05", color=f_tint(4, a04, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p05 = plot(s05, "Spectrum 06", color=f_tint(5, a05, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p06 = plot(s06, "Spectrum 07", color=f_tint(6, a06, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p07 = plot(s07, "Spectrum 08", color=f_tint(7, a07, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p08 = plot(s08, "Spectrum 09", color=f_tint(8, a08, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p09 = plot(s09, "Spectrum 10", color=f_tint(9, a09, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p10 = plot(s10, "Spectrum 11", color=f_tint(10, a10, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p11 = plot(s11, "Spectrum 12", color=f_tint(11, a11, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p12 = plot(s12, "Spectrum 13", color=f_tint(12, a12, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p13 = plot(s13, "Spectrum 14", color=f_tint(13, a13, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p14 = plot(s14, "Spectrum 15", color=f_tint(14, a14, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p15 = plot(s15, "Spectrum 16", color=f_tint(15, a15, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p16 = plot(s16, "Spectrum 17", color=f_tint(16, a16, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p17 = plot(s17, "Spectrum 18", color=f_tint(17, a17, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p18 = plot(s18, "Spectrum 19", color=f_tint(18, a18, false), linewidth=ribbonWidth, editable=false, display=display.pane)
p19 = plot(s19, "Spectrum 20", color=f_tint(19, a19, false), linewidth=ribbonWidth, editable=false, display=display.pane)
fill(p00, p01, color=showFill ? f_tint(0, (a00 + a01) / 2.0, true) : na, title="Weave 01", editable=false)
fill(p01, p02, color=showFill ? f_tint(1, (a01 + a02) / 2.0, true) : na, title="Weave 02", editable=false)
fill(p02, p03, color=showFill ? f_tint(2, (a02 + a03) / 2.0, true) : na, title="Weave 03", editable=false)
fill(p03, p04, color=showFill ? f_tint(3, (a03 + a04) / 2.0, true) : na, title="Weave 04", editable=false)
fill(p04, p05, color=showFill ? f_tint(4, (a04 + a05) / 2.0, true) : na, title="Weave 05", editable=false)
fill(p05, p06, color=showFill ? f_tint(5, (a05 + a06) / 2.0, true) : na, title="Weave 06", editable=false)
fill(p06, p07, color=showFill ? f_tint(6, (a06 + a07) / 2.0, true) : na, title="Weave 07", editable=false)
fill(p07, p08, color=showFill ? f_tint(7, (a07 + a08) / 2.0, true) : na, title="Weave 08", editable=false)
fill(p08, p09, color=showFill ? f_tint(8, (a08 + a09) / 2.0, true) : na, title="Weave 09", editable=false)
fill(p09, p10, color=showFill ? f_tint(9, (a09 + a10) / 2.0, true) : na, title="Weave 10", editable=false)
fill(p10, p11, color=showFill ? f_tint(10, (a10 + a11) / 2.0, true) : na, title="Weave 11", editable=false)
fill(p11, p12, color=showFill ? f_tint(11, (a11 + a12) / 2.0, true) : na, title="Weave 12", editable=false)
fill(p12, p13, color=showFill ? f_tint(12, (a12 + a13) / 2.0, true) : na, title="Weave 13", editable=false)
fill(p13, p14, color=showFill ? f_tint(13, (a13 + a14) / 2.0, true) : na, title="Weave 14", editable=false)
fill(p14, p15, color=showFill ? f_tint(14, (a14 + a15) / 2.0, true) : na, title="Weave 15", editable=false)
fill(p15, p16, color=showFill ? f_tint(15, (a15 + a16) / 2.0, true) : na, title="Weave 16", editable=false)
fill(p16, p17, color=showFill ? f_tint(16, (a16 + a17) / 2.0, true) : na, title="Weave 17", editable=false)
fill(p17, p18, color=showFill ? f_tint(17, (a17 + a18) / 2.0, true) : na, title="Weave 18", editable=false)
fill(p18, p19, color=showFill ? f_tint(18, (a18 + a19) / 2.0, true) : na, title="Weave 19", editable=false)
// END GENERATED VISUALS
//==================================================================== alerts
// Four conditions keep the budget at 63 of 64. Spectrum events use the alert() stream.
bool gate = (not iAlertQual or nowQual) and gradePass
alertcondition(emit == 1 and gate, "Bullish rejection", "Bullish rejection | {{ticker}} | {{interval}} | close {{close}}")
alertcondition(emit == 2 and gate, "Bearish rejection", "Bearish rejection | {{ticker}} | {{interval}} | close {{close}}")
alertcondition(emit == 3 and gate, "Bullish acceptance", "Bullish acceptance | {{ticker}} | {{interval}} | close {{close}}")
alertcondition(emit == 4 and gate, "Bearish acceptance", "Bearish acceptance | {{ticker}} | {{interval}} | close {{close}}")
if iSpectrumAlerts
if bullEvent
alert("Konatus Spectrum: confirmed bullish trend | " + syminfo.ticker + " " + timeframe.period, alert.freq_once_per_bar_close)
if bearEvent
alert("Konatus Spectrum: confirmed bearish trend | " + syminfo.ticker + " " + timeframe.period, alert.freq_once_per_bar_close)
if recoveryEvent
alert("Konatus Spectrum: confirmed pullback recovery | " + syminfo.ticker + " " + timeframe.period, alert.freq_once_per_bar_close)
if fractureEvent
alert("Konatus Spectrum: slow trend agreement deteriorating | " + syminfo.ticker + " " + timeframe.period, alert.freq_once_per_bar_close)
//================================================================= dashboard
if iShowDash and barstate.islast
if na(dash)
dash := table.new(position.top_right, 2, 12, bgcolor = C_BG,
frame_color = color.new(C_RANGE, 60), frame_width = 1,
border_color = color.new(C_RANGE, 70), border_width = 1)
color accent = direction > 0 ? #4DE3D0 : direction < 0 ? #EF5DA8 : #FFD166
string bias = direction > 0 ? "BULL" : direction < 0 ? "BEAR" : "MIXED"
string phase = state == "Warming up" ? str.tostring(math.max(0, longestLength * 3 - bar_index)) + " bars to warmup" : state
string stat = "seeking"
if rstate == 1
stat := fmtPrice(rLo) + " - " + fmtPrice(rHi)
else if iMode == "Manual"
stat := manualDone ? "spent, set new levels" : iManHi <= iManLo ? "manual levels invalid" : "waiting for activation"
else if lastRetire > 0 and bar_index - lastRetire <= iN
stat := "cooldown, " + lastRetireWhy
string pend = "bear acc, awaiting close"
if ss == 0
pend := "none"
else if ss == 1
pend := "bull rej, awaiting reclaim"
else if ss == 2
pend := "bull rej, awaiting close"
else if ss == 3
pend := "bear rej, awaiting reclaim"
else if ss == 4
pend := "bear rej, awaiting close"
else if ss == 5
pend := "bull acc, awaiting retest"
else if ss == 6
pend := "bull acc, awaiting close"
else if ss == 7
pend := "bear acc, awaiting retest"
string sig = "none"
string note = "-"
if lastSigBar >= 0
sig := lastSigKind + " " + fmtR(lastSigR)
note := (lastSigQual ? "qualified" : lastSigWhy) + ", " + lastSigGrade
table.cell(dash, 0, 0, "KONATUS", text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 1, 0, "SPECTRUM + RAR", text_color = #A775FF, text_size = txtSize)
table.cell(dash, 0, 1, "trend", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 1, bias + " · " + phase, text_color = accent, text_size = txtSize)
table.cell(dash, 0, 2, "trend health", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 2, health, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 3, "structure", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 3, closedStructure, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 4, "range", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 4, stat, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 5, "pending", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 5, pend, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 6, "last signal", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 6, sig, text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 7, "note", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 7, note, text_color = C_TEXT, text_size = txtSize)
if iShowScore
table.cell(dash, 0, 8, "with trend", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 8, fmtScore(withWins, withLoss, withSumR), text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 9, "no trend", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 9, fmtScore(flatWins, flatLoss, flatSumR), text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 10, "against trend", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 10, fmtScore(contraWins, contraLoss, contraSumR), text_color = C_TEXT, text_size = txtSize)
table.cell(dash, 0, 11, "score scope", text_color = color.new(C_TEXT, 40), text_size = txtSize)
table.cell(dash, 1, 11, (iScoreQual ? "qualified" : "all valid") + ", no costs, " + str.tostring(array.size(opStop)) + " open", text_color = C_TEXT, text_size = txtSize)
One chart · Two different jobs
Read Market Anatomy for context, then Konatus Bearing for its trading-plan conditions. The purpose of pairing them is to understand what you are looking at before interpreting a setup. You can also use either one on its own.
Understand the market
What is price doing? Read direction, pace, path organization, volatility and participation. Its explanations make conflicting evidence visible. Economic causes remain hypotheses.
Explore Market Anatomy →Review the plan
What is the setup waiting for? Read its entry status, named restrictions and any illustrative position, frozen stop and quantity.
Explore Konatus Bearing →Two standalone tools, shared evidence. Both analyze much of the same price history. Agreement is not two independent confirmations or a calibrated probability. The standalone scripts do not communicate: adding Anatomy does not change Bearing’s signals, stops or sizing. This manual pairing is a reading workflow, not a tested combined strategy.
Check the symbol, session, warmup and settings, then name the disagreement: direction, pace, extension or participation. Their formulas, thresholds and volume rules differ. Anatomy’s range scale is an SMA of true range; Bearing labels its distances in ATR. Their numbers are not interchangeable. Record the conflict rather than changing settings until they agree.
For Bearing, select Any alert() function call. For Anatomy, choose the individual direction, shock, breakout or invalidation condition and Once Per Bar Close. Label alerts with the indicator name. They report separate events; neither is a broker order.
Recreate alerts after changing the script, inputs, symbol or timeframe. Existing alerts keep the configuration saved when they were created. See TradingView’s alert documentation.
Start as a research exercise. Native TradingView compilation, chart behavior and alert delivery are still unverified. Once the scripts compile and run correctly, use replay or paper trading to assess the workflow. No trading edge or benefit from combining them has been established.
Build a readable workspace
You do not need Atlas, or both KONATUS tools, on every chart. Keep Bearing when you want its illustrative planning framework. Keep Anatomy when you want context for a method you already use. Add a companion only when it answers a specific question still missing from your workflow.
Location by price level
Bearing supplies its setup and plan conditions; Volume Profile shows how activity is distributed across price levels over a selected range. Define that range before interpreting the setup.
Activity depends on feed coverage and volume meaning. A high-volume area is not a guaranteed support level or independent proof of an entry.
About Volume Profile ↗Context for your trend method
Use Anatomy to read pace, path organization and shocks alongside an existing ATR-based trend method. Let your written method define decisions; Anatomy explains the observed conditions.
Both depend on price. This is a manual pairing, not an automated connection. If you use Supertrend instead of Bearing, you do not inherit Bearing’s sizing or ledger.
About Supertrend ↗A reference from a chosen event
An event-anchored reference answers a different question from a calendar reset: where is volume-weighted price since a specified point? Record an objective anchor rule, such as the first bar after a scheduled release.
Check volume semantics and avoid moving the anchor to fit the result. Hide an existing VWAP line if it only duplicates the reference.
About Anchored VWAP ↗An alternate definition to compare
RSI provides a price-momentum oscillator. Bollinger Bands use a moving-average and standard-deviation envelope. Choose one for a defined comparison with pace or relative location.
These overlap with existing price-derived evidence. An extreme reading is not automatically a reversal, and agreement is not another independent vote.
These are research workflow suggestions, not tested combination strategies. Companion availability and data coverage depend on your TradingView access and feed. Native validation of the KONATUS research scripts remains pending.
Applies to every indicator and companion strategy
All indicators, scanner readings, scripts, examples, alerts and accompanying material on this site are provided for education and independent research. They are not personalized financial, investment, tax or legal advice, a recommendation to trade, or an offer or solicitation to buy or sell an instrument. They do not assess your financial situation, objectives or ability to bear losses.
Trading and investing involve substantial risk. You can lose all capital committed; leveraged products can produce losses beyond your initial investment. False breakouts, delayed signals, volatility, gaps, liquidity constraints, fees and slippage can materially change results. Chart levels, illustrative sizing and alerts are not guaranteed prices, broker orders or substitutes for your own risk controls.
Past performance does not guarantee future results. Backtests, simulations, synthetic examples and reference-model outputs are not records of actual trading. They may benefit from hindsight and may not capture liquidity, execution costs, market impact or the financial and behavioral pressures of live losses. No representation is made that an account will achieve similar gains or avoid losses.
The tools are experimental and may contain errors. Read the verification status beside each release: local tests do not establish native TradingView compilation, alert delivery, live reliability or a market edge. Feed availability, revisions, missing data, settings, session choices and loaded history can change readings. Confirmed calculations may lag the market.
Heuristic scores describe the evidence used by a script; they are not calibrated probabilities. Historical frequencies and uncertainty intervals are conditional on the sampled data and assumptions. They may not generalize to another instrument, timeframe or market regime. No predictive accuracy, profitability or superiority to another tool is promised.
Validate the code and rules, account for realistic costs, and use replay or paper testing before deciding whether a tool is appropriate. Make your own decisions and consult a suitably qualified professional when you need personalized advice. Editorial artwork is illustrative, not evidence of trading performance. These tools are not affiliated with or endorsed by TradingView.
Further reading: SEC investor bulletin on performance claims · CFTC on online trading systems.