🆕
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()
}


