FlowscopeDocs

Swing levels

Turn confirmed swing highs and lows into support and resistance rays that run until price breaks them, then freeze as broken levels.

Old swing highs and lows are where stops collect and where traders expect reactions. This script keeps them on the chart for you. Every confirmed swing high becomes a resistance ray and every swing low a support ray, starting at the swing and running to the right. When price closes through a level, the ray is replaced by a fixed segment that ends at the break, drawn faded and dotted so you can still see where the level was.

Only a set number of active levels per side is kept, so the chart stays readable, and an alert tells you when a level breaks.

swing-levels.fs
script "Swing levels"

input (
  left = input.int(10, title: "Swing bars left", min: 1, max: 50)
  right = input.int(5, title: "Swing bars right", min: 1, max: 50)
  maxLevels = input.int(8, title: "Active levels per side", min: 1, max: 40)
  useWicks = input.bool(false, title: "Break on wick instead of close")
  keepBroken = input.bool(true, title: "Keep broken levels")
  resColor = input.color(color.red, title: "Resistance")
  supColor = input.color(color.green, title: "Support")
)

data chart = subscribe(data.ohlcv)

type Level {
  id: int
  price: float
  start: time
}

type Break {
  kind: string
  level: float
}

state (
  highs = Level[](maxLevels)
  lows = Level[](maxLevels)
  nextId = 0
  lines = entities.linePool(max: 300)
  tags = entities.labelPool(max: 100)
)

alert levelBreak = alert(title: "Swing level broken", onClose: true, payload: Break)

// A broken level: a faded segment up to the break, or nothing.
fn retire(lv: Level, t: time, tone: color) {
  tags.delete(lv.id)
  if keepBroken {
    lines.get(lv.id).set(lv.start, lv.price, t, lv.price, color: color.withAlpha(tone, 110), style: linestyle.dotted)
  } else {
    lines.delete(lv.id)
  }
}

// A new level: a ray from the swing to the right edge, with its price.
fn draw(lv: Level, t: time, tone: color, prefix: string) {
  lines.get(lv.id).set(lv.start, lv.price, t, lv.price, width: 2.0, color: tone, extend: extend.right)
  tags.get(lv.id).set(lv.start, lv.price, str.format("{0} {1:.2}", prefix, lv.price), color: tone)
}

on chart.close {
  let ph = ta.pivotHigh(chart.high, left, right)
  let pl = ta.pivotLow(chart.low, left, right)
  let t = chart.time
  let swingTime = chart.time[right]
  let up = useWicks ? chart.high : chart.close
  let down = useWicks ? chart.low : chart.close
  if t != null && up != null && down != null {

    // 1. Retire resistance levels that price has broken.
    let keepHighs = Level[](maxLevels)
    for lv in highs {
      if up > lv.price {
        retire(lv, t, resColor)
        levelBreak.trigger(Break { kind: "resistance", level: lv.price })
      } else {
        keepHighs.push(lv)
      }
    }
    highs = keepHighs

    // 2. The same for support.
    let keepLows = Level[](maxLevels)
    for lv in lows {
      if down < lv.price {
        retire(lv, t, supColor)
        levelBreak.trigger(Break { kind: "support", level: lv.price })
      } else {
        keepLows.push(lv)
      }
    }
    lows = keepLows

    // 3. Add newly confirmed swings, dropping the oldest when full.
    if ph != null && swingTime != null {
      if highs.isFull {
        let old = highs.shift()
        if old != null {
          lines.delete(old.id)
          tags.delete(old.id)
        }
      }
      nextId += 1
      let lv = Level { id: nextId, price: ph, start: swingTime }
      highs.push(lv)
      draw(lv, t, resColor, "R")
    }
    if pl != null && swingTime != null {
      if lows.isFull {
        let old = lows.shift()
        if old != null {
          lines.delete(old.id)
          tags.delete(old.id)
        }
      }
      nextId += 1
      let lv = Level { id: nextId, price: pl, start: swingTime }
      lows.push(lv)
      draw(lv, t, supColor, "S")
    }
  }
}

How it works

Confirmed swings only

ta.pivotHigh(chart.high, left, right) returns the swing price once the bar right bars ago is the highest of its neighbourhood. The swing’s time is therefore read with the same offset, chart.time[right], so the ray starts at the actual swing candle even though it is drawn right bars later. Both pivot calls sit at the top of the handler, outside the null check, so they see every bar.

Rays for live levels, segments for history

A line with extend: extend.right continues to the right edge of the chart. Drawing it once, from the swing to the current bar at the same price, is enough; it needs no update on later bars.

When a level breaks, retire redraws the same pool entry as a faded, dotted segment from the swing to the break bar, or deletes it when broken levels are not kept. Each level’s id is its key in both pools, so creation, update and removal always agree.

Keeping the active list

Active levels are stored as Level records in two arrays with capacity maxLevels. Removing elements from an array while looping over it is easy to get wrong, so the script walks the array, handles broken levels, and pushes survivors into a local array, which then replaces the state array:

Flowscope Script
highs = keepHighs

New swings are appended at the end. When the array is full, shift() removes the oldest level, which is also the one furthest back in time, and its drawings are deleted with it. Pushing into a full array would stop the script, so the isFull check comes first.

If several levels break on the same bar, the alert still fires only once for that bar, because an alert fires at most once per bar.

Functions that draw

retire and draw read the pools and inputs directly; functions see the script’s state and setup values. Broken segments stay until the line pool’s 300 entries are used up; then the oldest is recycled.

Adapting it

  • Higher-timeframe swings. Subscribe to timeframe: 4h, run the pivot logic in on htf.close, and draw from the chart handler. See market structure for that pattern.
  • Liquidity sweeps instead of breaks. Require a wick through the level and a close back on the original side. That is a sweep of the stops above or below, not a break.
  • Touch counter. Add a touches: int field to Level, increment it when the bar’s range reaches the level without breaking it, and show the count in the label.
  • Volume filter. Only keep swings whose bar had above-average volume, using ta.sma(chart.volume, 20) evaluated on every bar.