Paylaşmak, beğenmek ve takip etmek için Temel üyeliğe kaydolabilirsiniz.Giriş yapın ya da üye olun
🆕
APRUSDT.F · 5m · 1 Kas 20261 Kas
Long/Short PressureGrafikte aç
Kodu görüntüle
Long/Short Pressure
// © Markuda · Markuda CodeScript

// ═══════════════════════════════════════════════════════════════════════════════
// LONG/SHORT PRESSURE
//
// On perpetuals, the crowded side pays funding to the other side.
// This indicator reads that payment together with price:
//
// - Funding is compared with its own values over the Lookback bars, not with a fixed number,
//   so the same settings fit large and small markets.
// - Pressure runs from -1 (shorts crowded) to +1 (longs crowded) and is drawn
//   in a pane below the chart.
// - When one side stays crowded and price moves less than the Minimum move setting, a squeeze
//   zone is drawn and the squeeze risk is marked on the chart.
// - Candles and the glow around price take the color of long or short
//   pressure; a deeper color means stronger pressure.
// - Outside a squeeze, the direction comes from the distance between a fast
//   and a slow average, measured in ATR.
//
// Funding data exists on perpetuals only. On a spot chart the panel
// says so and the pressure pane stays empty.
// It describes positioning; it does not give trading signals.
// ═══════════════════════════════════════════════════════════════════════════════

// Compares funding on perpetuals with its own values over the Lookback bars.
// It shows whether longs or shorts are crowded and how strong the pressure is.

indicator("Long/Short Pressure", "Long/Short Pressure", overlay = true, max_boxes_count = 60, max_labels_count = 80)

// These inputs set the fast and slow averages and the levels where the direction changes.
var trendFast = inputInt(21, "Fast average", min = 3, max = 200, step = 1, group = "Direction", tooltip = "Length of the fast average.")
var trendSlow = inputInt(55, "Slow average", min = 10, max = 400, step = 1, group = "Direction", tooltip = "Length of the slow average.")
var trendFull = inputFloat(1.5, "Full trend (× ATR)", min = 0.2, max = 10.0, step = 0.1, group = "Direction", tooltip = "Distance between the two averages, in ATR, that counts as a full-strength trend.")
var stateEnter = inputFloat(0.4, "Trend entry level", min = 0.05, max = 0.95, step = 0.05, group = "Direction", tooltip = "Outside a squeeze, trend strength must reach this level to show long or short pressure.")
var stateExit = inputFloat(0.1, "Trend exit level", min = 0.0, max = 0.9, step = 0.05, group = "Direction", tooltip = "Outside a squeeze, the direction returns to Neutral when trend strength drops below this level.")
// This input sets the minimum number of bars between two squeeze marks.
var minGap = inputInt(6, "Bars between squeeze marks", min = 1, max = 200, step = 1, group = "Squeeze", tooltip = "Minimum bars between two squeeze marks.")
// These inputs set how funding is compared and when a side counts as crowded.
var normLen = inputInt(240, "Lookback (bars)", min = 50, max = 2000, step = 10, group = "Pressure", tooltip = "Number of bars of funding history used for comparison.")
var progressBars = inputInt(8, "Crowded bars", min = 3, max = 200, step = 1, group = "Pressure", tooltip = "How many bars one side must stay crowded before the price move is measured.")

// Values are carried from one bar to the next.
type Crowd {
  float rate = na
  float press = na
  float prevPress = na
  float usual = na
  float mean = 0.0
  float dev = 0.0
  bool pressSeen = false
  float range = na
  float fast = na
  float slow = na
  float mid = na
  float glow = 0.0
  bool hasFunding = false
  int runSide = 0
  int runStart = 0
  float runClose = na
  int zoneSide = 0
  int zoneStart = 0
  float zoneTop = na
  float zoneBottom = na
  float score = 0.0
  int trendDir = 0
  int holdSide = 0
  int dir = 0
  int dirStart = 0
  int toneDir = 9
  float toneT = 0.0
  float toneGlow = 0.0
  int filled = 0
  string bodyUp = na
  string bodyDown = na
  string body = na
  string halo = na
  string core = na
  float glowTop = na
  float glowBottom = na
  float coreTop = na
  float coreBottom = na
  int markSide = 0
  int squeezes = 0
  int rowShift = 0
  int markBar = na
  float markPrice = na
  float total = 0.0
  float totalSq = 0.0
  array window = na
  array ranked = na
  array zones = na
  array marks = na
  array badges = na
  table panel = na
}

var st = Crowd.new(window = arrayNewFloat(), ranked = arrayNewFloat(), zones = arrayNew(), marks = arrayNew(), badges = arrayNew())

// These inputs choose what appears on the chart and where the panel sits.
var showCandles = inputBool(true, "Color the candles", group = "On the chart", tooltip = "Candles take the color of the direction; the deeper the color, the stronger the state.")
var showGlow = inputBool(true, "Glow around price", group = "On the chart", tooltip = "A glow around price that widens and deepens with the strength of the direction.")
var showMarks = inputBool(true, "Squeeze and direction marks", group = "On the chart", tooltip = "Shows where squeeze risk appears and where the direction changes.")
var showZones = inputBool(true, "Squeeze zones", group = "On the chart", tooltip = "Zones where longs or shorts stayed crowded while price moved less than the Minimum move.")
var showPane = inputBool(true, "Pressure pane", group = "On the chart", tooltip = "Funding pressure from -1 to +1 in a pane below the chart.")
var showPanel = inputBool(true, "Summary panel", group = "On the chart", tooltip = "Shows the direction, the last squeeze, the crowded side and funding.")
var panelSize = inputString("Normal", "Panel size", group = "On the chart", tooltip = "Sets the size of the summary panel.", options = ["Small", "Normal", "Large"])
var panelSpot = inputString("Top right", "Panel position", group = "On the chart", tooltip = "Sets the position of the summary panel on the chart.", options = ["Top right", "Top left", "Bottom right", "Bottom left"])
var normMode = inputString("Standard", "Comparison", group = "Pressure", tooltip = "Standard compares funding with its average over the Lookback bars. Rank compares it with each value over the Lookback bars.", options = ["Standard", "Rank"])
var enterLevel = inputFloat(0.4, "Crowded level", min = 0.2, max = 0.95, step = 0.05, group = "Pressure", tooltip = "Above this level, longs or shorts count as crowded. The full scale is 1.")
var exitLevel = inputFloat(0.2, "Exit level", min = 0.0, max = 0.9, step = 0.05, group = "Pressure", tooltip = "Crowding ends when pressure drops back below this level, closing the squeeze zone.")
var calmLevel = inputFloat(0.25, "Balanced zone", min = 0.0, max = 0.5, step = 0.05, group = "Pressure", tooltip = "Pressure inside this zone is drawn grey in the pane.")
var progressAtr = inputFloat(1.0, "Minimum move (× ATR)", min = 0.1, max = 10.0, step = 0.1, group = "Pressure", tooltip = "Price must move at least this many ATR in the crowded side's direction; otherwise a squeeze zone is drawn.")
// These inputs set the colors for long pressure, short pressure, crowded sides and the Neutral direction.
var colBull = inputColor("#1fd18a", "Long pressure", group = "Look", tooltip = "Color of long pressure and of short squeeze marks.")
var colBear = inputColor("#ff4d6a", "Short pressure", group = "Look", tooltip = "Color of short pressure and of long squeeze marks.")
var colHot = inputColor("#ff6a2e", "Longs crowded", group = "Look", tooltip = "Pane and zone color when longs are crowded.")
var colWarm = inputColor("#ffb347", "Longs leaning", group = "Look", tooltip = "Pane color when longs are leaning.")
var colCold = inputColor("#3d7bff", "Shorts crowded", group = "Look", tooltip = "Pane and zone color when shorts are crowded.")
var colCool = inputColor("#6fd0ff", "Shorts leaning", group = "Look", tooltip = "Pane color when shorts are leaning.")
var colCalm = inputColor("#8a8f9c", "Neutral", group = "Look", tooltip = "Used for the Neutral direction and the Balanced zone.")

// The pressure scale, the ATR length, the colors and the number of marks on the chart are set here.
var Z_FULL = 3.0
var EPSILON = 0.000001
var RATE_SCALE = 10000.0
var ATR_LENGTH = 14
var MID_LENGTH = 4
var GLOW_LENGTH = 6
var MARK_LEVEL = 1.12
var BULL_R = 31
var BULL_G = 209
var BULL_B = 138
var BEAR_R = 255
var BEAR_G = 77
var BEAR_B = 106
var CALM_R = 128
var CALM_G = 134
var CALM_B = 148
var KEPT = 6
var MARKS_KEPT = 12
var PANEL_GROUND = "#0f1218"
var PANEL_TEXT = "#d7dae0"
var PANEL_MUTED = "#8a8f9c"
var SPOT_NOTICE = "Perpetuals only: open the .F chart for funding data."
var NOTICE_TINT = "#ffb347"
var INK = "#ffffff"
var LONG_INK = "#06281b"
var DOWN_FADE = 45
// Candle colors update only when the color changes by at least this step.
var TONE_STEP = 0.05
var MID_RANK = floor(normLen / 2)
var FAST_K = 2.0 / (trendFast + 1)
var SLOW_K = 2.0 / (trendSlow + 1)
var MID_K = 2.0 / (MID_LENGTH + 1)
var GLOW_K = 2.0 / (GLOW_LENGTH + 1)
var CLEAR = colorNew(INK, 100)
var CALM_PANE = colorNew(colCalm, 60)
var LONG_EDGE = colorNew(colHot, 35)
var SHORT_EDGE = colorNew(colCold, 35)
var USE_RANK = normMode == "Rank"

// Numbers in the panel and the tooltips are formatted here.
fn halfUp(x) => floor(x + 0.5)
fn pad(n, width) {
  s = toString(n)
  while strLength(s) < width {
    s := "0" + s
  }
  return s
}
fn grouped(n) => n >= 1000 ? grouped(floor(n / 1000)) + "," + pad(n % 1000, 3) : toString(n)
fn unsigned(x, places) {
  unit = pow(10, places)
  v = halfUp(abs(x) * unit)
  return grouped(floor(v / unit)) + (places > 0 ? "." + pad(v % unit, places) : "")
}
fn fixed(x, places) => (x < 0 ? "-" : "+") + unsigned(x, places)
fn priceText(x) => unsigned(x, x >= 1000 ? 1 : x >= 10 ? 2 : x >= 1 ? 4 : 6)
fn ratePct(r) => fixed(r * 100, 4) + "%"
fn bars(n) => grouped(n) + (n == 1 ? " bar" : " bars")
// The words and colors for the direction, the squeeze risk and the crowded side are set here.
fn dirName(d) => d == 1 ? "Long pressure" : d == -1 ? "Short pressure" : "Neutral"
fn dirWord(d) => d == 1 ? "LONG PRESSURE" : d == -1 ? "SHORT PRESSURE" : "NEUTRAL"
fn squeezeWord(d) => d == 1 ? "Short squeeze risk" : "Long squeeze risk"
fn dirColor(d) => d == 1 ? colBull : d == -1 ? colBear : colCalm
fn crowdWord(s) => s == 1 ? "Longs crowded" : s == -1 ? "Shorts crowded" : "Balanced"
fn crowdColor(s) => s == 1 ? colHot : s == -1 ? colCold : PANEL_TEXT
// Candle and glow colors for a direction; a stronger direction gives a deeper color.
fn shade(d, t) {
  r = d == 1 ? BULL_R : d == -1 ? BEAR_R : CALM_R
  g = d == 1 ? BULL_G : d == -1 ? BEAR_G : CALM_G
  b = d == 1 ? BULL_B : d == -1 ? BEAR_B : CALM_B
  rr = CALM_R + (r - CALM_R) * t
  gg = CALM_G + (g - CALM_G) * t
  bb = CALM_B + (b - CALM_B) * t
  st.bodyUp := colorRgb(rr, gg, bb, 0)
  st.bodyDown := colorRgb(rr, gg, bb, DOWN_FADE)
  st.halo := colorRgb(rr, gg, bb, 93 - 13 * st.glow)
  st.core := colorRgb(rr, gg, bb, 88 - 18 * st.glow)
}

// Only the latest zones and marks stay on the chart; older ones are removed.
fn keep(arr, limit) {
  while arraySize(arr) > limit {
    x = arrayShift(arr)
    if not na(x) {
      boxDelete(x)
    }
  }
}
fn keepLabels(arr, limit) {
  while arraySize(arr) > limit {
    labelDelete(arrayShift(arr))
  }
}
// Funding is scaled from -1 to +1, relative to its values over the Lookback bars.
fn pressureOf(f) {
  scaled = f * RATE_SCALE
  arrayPush(st.window, scaled)
  full = st.filled == normLen
  old = 0.0
  if full {
    old := arrayShift(st.window)
  }
  if not full or old != scaled {
    arrayInsert(st.ranked, arrayBinarySearchLeftmost(st.ranked, scaled), scaled)
    st.total := st.total + scaled
    st.totalSq := st.totalSq + scaled * scaled
    if full {
      arrayRemove(st.ranked, arrayBinarySearchLeftmost(st.ranked, old))
      st.total := st.total - old
      st.totalSq := st.totalSq - old * old
    } else {
      st.filled := st.filled + 1
    }
    if st.filled == normLen {
      st.mean := st.total / normLen
      spread = st.totalSq / normLen - st.mean * st.mean
      st.dev := spread > 0 ? sqrt(spread) : 0.0
      st.usual := arrayGet(st.ranked, MID_RANK) / RATE_SCALE
    }
  }
  p = na
  if st.filled == normLen {
    raw = st.dev > EPSILON ? (scaled - st.mean) / (st.dev * Z_FULL) : 0.0
    if USE_RANK {
      raw := (arrayBinarySearchLeftmost(st.ranked, scaled) + arrayBinarySearchRightmost(st.ranked, scaled) + 1) / normLen - 1
    }
    p := raw > 1 ? 1.0 : raw < -1 ? -1.0 : raw
    st.pressSeen := true
  }
  return p
}

// A squeeze mark appears where the crowded side keeps paying and price moves less than the Minimum move setting.
fn squeeze(d) {
  st.markSide := d
  st.markBar := barIndex
  st.markPrice := close
  st.holdSide := d
  st.squeezes := st.squeezes + 1
  if showMarks {
    why = squeezeWord(d) + " · " + crowdWord(-d) + " for " + bars(barIndex - st.runStart + 1) + " · funding " + ratePct(st.rate) + "\nPrice moved less than " + toString(progressAtr) + " ATR"
    arrayPush(st.badges, labelNew(d == 1 ? low - st.range * 0.6 : high + st.range * 0.6, d == 1 ? "▲ SQUEEZE" : "▼ SQUEEZE", barIndex, color = dirColor(d), textcolor = d == 1 ? LONG_INK : INK, style = d == 1 ? label.style_label_up : label.style_label_down, size = size.normal, tooltip = why))
    keepLabels(st.badges, KEPT)
  }
}

// A small mark appears where the direction changes with the trend.
fn turn(d) {
  if showMarks and d != 0 and (na(st.markBar) or barIndex - st.markBar >= minGap) {
    arrayPush(st.marks, labelNew(d == -1 ? high + st.range * 0.3 : low - st.range * 0.3, "◆", barIndex, color = colorNew(PANEL_GROUND, 100), textcolor = dirColor(d), style = d == -1 ? label.style_label_down : label.style_label_up, size = size.tiny, tooltip = "Direction changed to " + dirName(d) + " with the trend"))
    keepLabels(st.marks, MARKS_KEPT)
  }
}

// The squeeze zone covers the bars where one side stayed crowded.
fn openZone(s) {
  st.zoneSide := s
  st.zoneStart := st.runStart
  st.zoneTop := high
  st.zoneBottom := low
  for k = 1 to barIndex - st.runStart {
    st.zoneTop := max(st.zoneTop, high[k])
    st.zoneBottom := min(st.zoneBottom, low[k])
  }
  if na(st.markBar) or barIndex - st.markBar >= minGap {
    squeeze(-s)
    if showZones {
      c = crowdColor(s)
      arrayPush(st.zones, boxNew(st.zoneStart, st.zoneTop, barIndex, st.zoneBottom, bgcolor = colorNew(c, 88), border_color = colorNew(c, 35), border_width = 1))
      keep(st.zones, KEPT)
    }
  }
  alert("Long/Short Pressure: " + squeezeWord(-s), alert.freq_once_per_bar)
}

// The squeeze zone ends when crowding ends.
fn closeZone() {
  st.zoneSide := 0
}

// The direction comes from the trend, or from the squeeze risk while it is active.
fn steer() {
  spread = (st.fast - st.slow) / (st.range * trendFull)
  trendN = spread > 1 ? 1.0 : spread < -1 ? -1.0 : spread
  if st.trendDir != 1 and trendN >= stateEnter {
    st.trendDir := 1
  } else if st.trendDir != -1 and trendN <= -stateEnter {
    st.trendDir := -1
  } else if st.trendDir != 0 and stateExit > trendN * st.trendDir {
    st.trendDir := 0
  }
  next = st.holdSide != 0 ? st.holdSide : st.trendDir
  held = 0.5 + 0.5 * st.holdSide * trendN
  st.score := st.holdSide != 0 ? (held < 0.1 ? 0.1 : held) : trendN < 0 ? -trendN : trendN
  if next != st.dir {
    st.dir := next
    st.dirStart := barIndex
    if barIndex != st.markBar {
      turn(next)
    }
  }
}

// The panel uses the size and position chosen in the inputs.
fn rowSize() => panelSize == "Small" ? size.tiny : panelSize == "Large" ? size.normal : size.small
fn titleSize() => panelSize == "Small" ? size.normal : panelSize == "Large" ? size.huge : size.large
fn panelPlace() => panelSpot == "Top left" ? position.top_left : panelSpot == "Bottom right" ? position.bottom_right : panelSpot == "Bottom left" ? position.bottom_left : position.top_right

// Each panel row has a name and a value; on a spot chart every row sits one line lower.
fn row(r, name, value, tint) {
  at = r + st.rowShift
  tableCell(st.panel, 0, at, name, text_color = PANEL_MUTED, text_size = rowSize(), bgcolor = colorNew(PANEL_GROUND, 6))
  tableCell(st.panel, 1, at, value, text_color = tint, text_size = rowSize(), bgcolor = colorNew(PANEL_GROUND, 6))
  tableCellSetTextHalign(st.panel, 0, at, text.align_left)
  tableCellSetTextHalign(st.panel, 1, at, text.align_right)
}

// This row shows the last squeeze and the number of squeeze zones on the chart.
fn drawSqueezeRows() {
  if st.markSide == 0 {
    row(2, "Last squeeze", "None in " + bars(barIndex + 1) + (st.pressSeen ? " · no crowded side" : ""), PANEL_MUTED)
  } else {
    row(2, "Last squeeze", squeezeWord(st.markSide) + " at " + priceText(st.markPrice) + " · " + bars(barIndex - st.markBar) + " ago", dirColor(st.markSide))
  }
  row(3, "Squeeze zones", grouped(st.squeezes) + " in " + bars(barIndex + 1), st.squeezes == 0 ? PANEL_MUTED : PANEL_TEXT)
}

// This row shows the crowded side, or the reason funding cannot be read on this chart.
fn crowdText() {
  t = "No funding data · perpetuals only"
  if st.pressSeen {
    t := crowdWord(st.runSide)
  } else if st.hasFunding {
    t := "Not enough funding history on this chart. Try a lower timeframe."
  }
  return t
}

// The summary panel shows the direction, the last squeeze, the crowded side and funding.
fn drawPanel() {
  if na(st.panel) {
    st.rowShift := st.hasFunding ? 0 : 1
    st.panel := tableNew(panelPlace(), 2, 6 + st.rowShift, bgcolor = colorNew(PANEL_GROUND, 6), border_width = 0, frame_width = 1, frame_color = colorNew(PANEL_MUTED, 75))
    tableMergeCells(st.panel, 0, 0, 1, 0)
    if st.rowShift == 1 {
      tableMergeCells(st.panel, 0, 1, 1, 1)
      tableCell(st.panel, 0, 1, SPOT_NOTICE, text_color = NOTICE_TINT, text_size = rowSize(), bgcolor = colorNew(NOTICE_TINT, 85))
    }
  }
  d = st.dir
  tableCell(st.panel, 0, 0, dirWord(d) + "  " + toString(halfUp(st.score * 100)) + "%", text_color = d == 0 ? PANEL_TEXT : INK, text_size = titleSize(), bgcolor = colorNew(dirColor(d), d == 0 ? 70 : 15))
  row(1, "Since", bars(barIndex - st.dirStart) + " · " + (st.holdSide != 0 ? "squeeze risk" : "trend"), PANEL_TEXT)
  drawSqueezeRows()
  row(4, "Crowded side", crowdText(), st.pressSeen ? crowdColor(st.runSide) : PANEL_MUTED)
  row(5, "Funding · usual", st.pressSeen ? ratePct(st.rate) + " · " + ratePct(st.usual) : "—", st.pressSeen ? crowdColor(st.runSide) : PANEL_MUTED)
}

// On every bar the indicator updates ATR, the averages and the funding pressure.
f = funding()
first = barIndex == 0
trueRange = first ? high - low : max(high, close[1]) - min(low, close[1])
st.range := first ? trueRange : st.range + (trueRange - st.range) / ATR_LENGTH
st.fast := first ? close : st.fast + (close - st.fast) * FAST_K
st.slow := first ? close : st.slow + (close - st.slow) * SLOW_K
st.mid := first ? hl2 : st.mid + (hl2 - st.mid) * MID_K
st.rate := f
st.prevPress := st.press
st.press := na
hasPress = false
if not na(f) {
  st.hasFunding := true
  st.press := pressureOf(f)
  hasPress := st.pressSeen
}
press = st.press

// A side counts as crowded while pressure stays above the Crowded level.
if hasPress {
  side = press >= enterLevel and f > st.usual ? 1 : press <= -enterLevel and f < st.usual ? -1 : 0
  if st.runSide != 0 and exitLevel > press * st.runSide {
    if st.zoneSide != 0 {
      closeZone()
    }
    st.runSide := 0
    st.holdSide := 0
  }
  if st.runSide == 0 and side != 0 {
    st.runSide := side
    st.runStart := barIndex
    st.runClose := close
  }
  if st.runSide != 0 and st.zoneSide == 0 and barIndex - st.runStart + 1 >= progressBars and progressAtr > st.runSide * (close - st.runClose) / st.range {
    openZone(st.runSide)
  }
}
// This step sets the direction and its strength for this bar.
steer()

// Candle and glow colors match the direction and its strength.
strength = st.score
t = st.dir == 0 ? 0.0 : 0.35 + 0.65 * strength
st.glow := st.glow + ((st.dir == 0 ? 0.0 : strength) - st.glow) * GLOW_K
dt = t - st.toneT
dg = st.glow - st.toneGlow
if st.dir != st.toneDir or dt >= TONE_STEP or dt <= -TONE_STEP or dg >= TONE_STEP or dg <= -TONE_STEP {
  st.toneDir := st.dir
  st.toneT := t
  st.toneGlow := st.glow
  shade(st.dir, t)
}
st.body := close >= open ? st.bodyUp : st.bodyDown
half = st.range * (0.7 + 2.6 * st.glow)
st.glowTop := st.mid + half
st.glowBottom := st.mid - half
st.coreTop := st.mid + half * 0.5
st.coreBottom := st.mid - half * 0.5

// This step draws the candle colors and the glow around price.
if showCandles {
  barcolor(st.body)
}
if showGlow {
  haloTop = plot(st.glowTop, "Glow top", color = CLEAR, pane = "price", axisLabel = false)
  haloBottom = plot(st.glowBottom, "Glow bottom", color = CLEAR, pane = "price", axisLabel = false)
  coreTop = plot(st.coreTop, "Core top", color = CLEAR, pane = "price", axisLabel = false)
  coreBottom = plot(st.coreBottom, "Core bottom", color = CLEAR, pane = "price", axisLabel = false)
  fill(haloTop, haloBottom, st.halo)
  fill(coreTop, coreBottom, st.core)
}

// The pressure pane draws funding pressure with the crowded levels for longs and shorts.
if showPane and hasPress {
  depth = press < 0 ? -press : press
  calm = depth < calmLevel
  opposed = st.dir != 0 and press * st.dir > 0
  heat = press >= enterLevel ? colHot : press >= 0 ? colWarm : press <= -enterLevel ? colCold : colCool
  plot(press, "Pressure", color = calm ? CALM_PANE : colorNew(heat, opposed ? 78 : 50 - 50 * depth), pane = "crowd", style = plot.style_columns, axisLabel = false)
  plot(enterLevel, "Longs crowded", color = LONG_EDGE, pane = "crowd", linestyle = "dashed", axisLabel = false)
  plot(-enterLevel, "Shorts crowded", color = SHORT_EDGE, pane = "crowd", linestyle = "dashed", axisLabel = false)
  crossedUp = not na(st.prevPress) and press >= enterLevel and enterLevel > st.prevPress
  crossedDown = not na(st.prevPress) and press <= -enterLevel and st.prevPress > -enterLevel
  plot(crossedUp ? MARK_LEVEL : crossedDown ? -MARK_LEVEL : na, "Crowded level crossed", color = crossedUp ? colHot : colCold, pane = "crowd", style = plot.style_circles, linewidth = 4, axisLabel = false)
}

// The panel appears once, on the last bar.
if barstate.islast and showPanel {
  drawPanel()
}