CX Script v1

Write your own indicators.

The built-in indicators cover the basics. CX Script is for everything past that: your own signals, in plain JavaScript, running live on the footprint chart and the historical chart. No build step, no account, no cost.

The language is ordinary JavaScript on purpose. It is what code assistants write best, so a working indicator is often one prompt away. Paste the API into any assistant, describe the idea, drop the result into the editor.

Plain JavaScript

ES2020, strict mode, a tight API. No DSL to learn, no compile.

Sandboxed

Each script runs in its own Web Worker. A runaway loop hits a budget and gets killed, never the chart.

Overlay or sub pane

Overlay the price chart or claim a dedicated sub pane. The footprint chart has a Script subplot, the historical chart takes up to two script panes.

Free

No premium gate. Scripting is part of the terminal for everyone.

Footprint vs historical chart

Same language, same API, same TA library on both charts. What differs is the data underneath. The historical chart has exchange klines and nothing else, so anything that needs per-trade size, footprint levels or the order book reads NaN or empty there. A script never errors because of it: the missing series plot gaps, and the legend badge reads partial when a script asks for footprint-only data on a klines chart.

Feed / fieldFootprint chartHistorical chart
candles() open, high, low, close, volume, tradeCountBuilt from ticks, liveExchange klines
buyVolume / sellVolume / volumeDelta() / cumulativeDelta()Tick-classified aggressor volumeTaker-buy kline field, bar level: sell = volume - takerBuy, delta = 2 * takerBuy - volume
volumeDelta({ tiers }) / cumulativeDelta({ tiers }) size tiersLiveNaN series, there are no per-trade sizes in a kline
footprintLevels()LiveEmpty
openInterest() / liquidations()Live over the retained windowEmpty / NaN, not sourced yet
funding() / orderbook() / bigTrades()Not sourced yet, NaNNot sourced yet, NaN
remote()WorksWorks
ta.* (all 36, including ta.vwap and ta.ema)AllAll
chart.aggModetime, tick, volume, range, delta, trAlways "time"
chart.symbolBase asset, e.g. "BTC"Venue-native, e.g. "BTCUSDT"
chart.venuePrimary leg venueThe chart's exchange: binancef, kraken, and so on
History depthThe live retained window, plus the history sourceUp to 5,000 klines. << Load More grows it and restarts the script over the longer history
Sub pane (pane: "sub")Script subplotUp to 2 script panes
Legend status badgereview / loading / warming / live / errSame, plus partial when the script asks for footprint-only data

A script written for the footprint chart runs unchanged on the historical chart. Read the historical column before you trust a number: a tiered delta line that is flat and empty there is the documented behaviour, not a bug.

Quickstart

Open the indicators panel on a footprint or historical chart, add a script, and paste this in. It reads per-bar delta as a share of volume, colours it by sign, and draws a smoothed line underneath. Save (Ctrl+S) and it warms up over history, then updates live.

  1. //@cx v1 The first line. A version pragma so future changes never silently break an old script.
  2. study(name, opts) Names the indicator and picks its default pane: "overlay" (price chart) or "sub".
  3. input.*, candles(), feeds Top-level declarations. They run once, then freeze.
  4. onCandle(fn) Your per-bar logic. Read series, compute, draw.
delta-pressure.js
//@cx v1
study("Delta pressure", { pane: "sub" })

const len  = input.int("Smoothing", 14, { min: 2, max: 200 })
const bars = candles()
const vd   = volumeDelta()

onCandle((bar) => {
  const norm = vd(0) / bars.volume(0)          // per-bar delta as a share of volume
  plot("delta", norm, { color: norm >= 0 ? "#22c55e" : "#ef4444" })
  plot("delta_ma", ta.sma(vd.map(v => v / bars.volume(0)), len), { color: "#8ecae6", width: 2 })
})

How it works

Top level runs once to collect declarations. The chart then serializes the feeds you asked for and hands them to the worker, which runs onCandle once per historical bar (warmup), then again for the live bar on every update. Drawing output streams back and the chart renders it from a cache, so rendering costs nothing per frame.

Scripts work at bar granularity. Updates batch at up to 10 Hz, so onCandle fires on the live bar many times, not once per trade. Gate once-per-bar logic on bar.isNew.

Script structure

A script runs in two passes. The top level is where declarations live, and it executes exactly once. Once it returns, the inputs, feeds and study call are sealed — reach for one from inside onCandle and the run stops with an error.

structure.js
//@cx v1
study("My indicator", { pane: "overlay" })   // or pane: "sub"

// top level runs ONCE — declarations only
const len  = input.int("Length", 14, { min: 2, max: 200 })
const bars = candles()

// per bar — runs during warmup then on every live flush
onCandle((bar) => {
  plot("sma", ta.sma(bars.close, len), { color: "#8ecae6" })
})

The bar object

onCandle receives the bar being processed. Plots and entities written for a bar are last-write-wins within that bar.

onCandle((bar) => { /* ... */ })
// bar.index      absolute bar index, 0 = oldest loaded bar
// bar.unix       bar open time, unix SECONDS (matches the chart X axis)
// bar.isNew      true the first time this bar is processed
// bar.isLast     true if this is the newest bar
// bar.isWarmup   true during warmup, false for live updates
The global chart context

A read-only chart object is always in scope: chart.symbol, chart.venue, chart.intervalSec, chart.aggMode, chart.tickSize, chart.barCount, and the theme candle colours chart.upColor / chart.downColor.

Two fields read differently per chart. chart.symbol is the base asset on the footprint chart ("BTC") and the venue-native symbol on the historical chart ("BTCUSDT"), so match on a prefix rather than an exact string if a script has to work on both. chart.aggMode is one of time, tick, volume, range, delta or tr on the footprint chart, and always "time" on the historical chart. chart.venue is the chart's own exchange on the historical chart (binancef, kraken, and so on).

Inputs

Declared at top level, each returns its current value. The terminal renders the settings panel from them automatically. Changing a value re-runs the script over the data it already has.

input.bool(label, def, opts?)                     // -> boolean
input.int(label, def, opts?)                      // -> number   opts: min, max, step
input.float(label, def, opts?)                    // -> number   opts: min, max, step
input.select(label, def, { options: string[] })   // -> string   def is the value, not an index
input.color(label, def, opts?)                    // -> "#RRGGBB" or "#RRGGBBAA"
input.string(label, def, opts?)                   // -> string   opts: secret
input.group(label)                                // section header, returns undefined

Shared options: key (persistence key, defaults to a slug of the label), inline: true (render on the same row as the previous input), and showIf: { key, eq | neq | in } for conditional visibility. Mark a string input secret: true and it is stripped from shared layouts.

Series

A Series is a callable: s(offset) returns its value that many bars back, where 0 is the current bar. Out of range is NaN. There is one index convention everywhere: offset from the current bar. Series are what the TA functions consume.

const bars = candles()
bars.close(0)                 // current bar's close; bars.close(1) = one bar back
bars.high.shift(1)            // a Series of prior-bar highs
vd.map(Math.abs)              // derived Series, lazy + memoized
series(i => bars.high(i) - bars.low(i))   // custom Series from an offset fn
bars.close.length             // bars available

Feeds

Feeds are the market data a script can subscribe to, declared at top level (max 12). Each takes an optional { venue } that defaults to the chart's primary exchange.

FeedReturnsDataFootprintHistorical
candles()open, high, low, close, volume, buyVolume, sellVolume, tradeCount, unix
Built from ticks on the footprint chart, exchange klines on the historical chart.
live
volumeDelta(opts?)per-bar delta Series (size tiers on the footprint chart)
Bar-level taker delta on the historical chart. Size tiers are NaN there.
live
cumulativeDelta(opts?)running delta sum Series
Bar-level taker delta on the historical chart. Size tiers are NaN there.
live
footprintLevels()per-price bid / ask / trades, plus poc(offset)
Empty on the historical chart, poc() is NaN.
liveno
openInterest()open, high, low, close
Not sourced on the historical chart yet.
liveno
liquidations()longVol, shortVol, longQty, shortQty
Not sourced on the historical chart yet.
liveno
orderbook()bestBid, bestAsk, mid, spread + bidDepth / askDepth / imbalance by band
Not sourced on either chart yet.
NaN
funding()rate, nextFundingTime (step series)
Not sourced on either chart yet.
NaN
bigTrades(opts?)list(offset) of prints: unix, price, qty, notional, side
Not sourced on either chart yet.
NaN
remote(url, opts)a bar-aligned Series from an external API live

Data column: live means the feed carries real data today. NaN means the API is wired but its sourcing lands in a future update, so the script still loads and runs, it just reads empty. Footprint / Historical says which chart supplies the feed. Scripts render on both charts; a feed marked no for historical reads NaN or empty there rather than erroring.

  • openInterest is in coin units (BTC, not USD — USD would move with price alone). It defaults to binancef and also accepts bybitf / hyperliquidf; any other venue returns NaN rather than an error. The OI venue is independent of the chart's venue, so a spot chart still reads perp OI — same rule as the OI subplot.
  • liquidations is one primary-symbol stream, so { venue } is accepted and ignored.
  • footprintLevels carries per-price rows for the most recent ~3,000 bars; older bars return an empty list and poc() = NaN.

Delta size tiers

On the footprint chart, volumeDelta and cumulativeDelta can filter by trade size. A kline carries no per-trade sizes, so on the historical chart every tiered read is a NaN series. The script loads, plots gaps, and the legend shows partial.

volumeDelta()                    // all sizes
volumeDelta({ tiers: [6, 7] })   // bucket index range, inclusive
volumeDelta({ tiers: "whale" })  // named groups:
//   "small" = < $100K     "mid" = $100K-$1M     "whale" = >= $1M
Honesty rules
  • Data availability today, on the footprint chart: candles, volumeDelta, cumulativeDelta, footprintLevels, openInterest and liquidations are live. Orderbook depth, funding and big trades return NaN / empty until their sourcing lands.
  • Orderbook depth returns NaN until sourcing lands.
  • The historical chart has no tick data. Candles come from exchange klines, delta comes from the bar's taker-buy field, and everything that needs per-trade size or the book reads NaN or empty there: size tiers, footprintLevels, openInterest and liquidations. Scripts still load and plot the series that do exist, they do not error, and the legend badge reads partial. Full breakdown in footprint vs historical chart.
  • History source applies to the footprint chart only; the historical chart always loads exchange klines. Every feed reads the chart's own stores, so it behaves identically under either history source — only the depth and the detail differ. Under Full history the archive covers the last 48 hours and its bars carry no per-trade detail: tradeCount and every tiered read (volumeDelta({ tiers }) / cumulativeDelta({ tiers })) are 0 there and only move once your session's live bars start. Plain delta and CVD work across the whole window. Under Live exchange tiers work over the BTC history prefix, and non-BTC symbols have no footprint history at all — the script warms up over whatever your session has built.
  • Liquidation retention is 8 days, open interest around 130k snapshots. Beyond that, NaN. A bar inside the retained window with no liquidations is a real 0; a bar older than the oldest retained event is NaN.
  • Accumulators are primary-symbol-centric. A symbol without data yields a NaN series. Nothing is ever fabricated.
  • TA seeding: ta.ema / ta.rma (and rsi/atr on top of them) seed with an SMA over the loaded window, while TradingView recurses over its full history — early bars can differ slightly and converge. A NaN entering an ema/rma window persists in the recursive state — clean your inputs before smoothing.

TA library

Around 36 functions on the ta namespace. Each takes a Series and returns a number at the current bar (a few return objects, noted with ->). They are pure functions of their inputs, safe inside conditionals; a period longer than the available history returns NaN rather than a partial window.

The ta.* names follow Pine Script conventions. If you have written a TradingView indicator — or you hand this page to an assistant that has seen thousands of them — the vocabulary already fits.

Averages     ta.sma(src,p)  ta.ema(src,p)  ta.rma(src,p)  ta.wma(src,p)  ta.hma(src,p)
             ta.vwma(src,vol,p)  ta.alma(src,p,offset=0.85,sigma=6)  ta.swma(src)
             ta.linreg(src,p,offset=0)
Oscillators  ta.rsi(src,p)  ta.stoch(high,low,close,p)  ta.mfi(high,low,close,vol,p)
             ta.roc(src,p)  ta.macd(src,fast,slow,sig) -> {macd,signal,hist}
             ta.dmi(high,low,close,p) -> {plus,minus,adx}
Volatility   ta.atr(high,low,close,p)  ta.tr(high,low,close)  ta.stdev(src,p)
             ta.variance(src,p)  ta.dev(src,p)  ta.bb(src,p,mult) -> {basis,upper,lower}
Aggregation  ta.highest(src,p)  ta.lowest(src,p)  ta.median(src,p)  ta.mode(src,p)
             ta.sum(src,p)  ta.pivothigh(src,left,right)  ta.pivotlow(src,left,right)
Change       ta.change(src,n=1)  ta.cum(src)  ta.barssince(condSeries)
             ta.rising(src,p)  ta.falling(src,p)
Crosses      ta.cross(a,b)  ta.crossover(a,b)  ta.crossunder(a,b)   // a,b: Series or number
VWAP         ta.vwap(high,low,close,vol,{ anchor: "day"|"week"|"session" }) -> number

fmt.price(v)    // tick-size-aware decimals
fmt.compact(v)  // 1.24M
fmt.pct(v, dp)  // percentage

Colors

Colours are hex strings everywhere ("#RRGGBB" or "#RRGGBBAA"). The color helpers build and blend them.

color.rgb(r, g, b, a=1)      color.alpha(c, a)          // a in [0,1], NOT a transparency %
color.lighten(c, pct)        color.darken(c, pct)       color.mix(a, b, t)
color.gradient(value, min, max, colorLo, colorHi)

Plotting

Drawing functions run inside onCandle. Each series has a stable id; the first call fixes its pane. A value of NaN leaves a gap, so conditional markers are just a value-or-NaN ternary.

plot(id, value, { color, width=1.5, style: "solid"|"dotted"|"dashed", pane })
histogram(id, value, { color, pane })
marker(id, yOrNaN, { shape, color, size=10, border, text, pane })
plotCandle(id, o, h, l, c, { up, down, pane })
bg(colorOrNaN)                                 // bar background tint
fill(idA, idB, { color })                      // both series must share a pane

// marker shapes:
//   circle  square  diamond  triangle-up  triangle-down
//   cross  plus  asterisk  arrow-up  arrow-down

Entities

Persistent, keyed drawing objects: lines, boxes, labels and markers positioned in chart coordinates. They upsert by key, so re-calling with the same key updates in place and hands back the same handle. Useful for levels, ranges and annotations that live across bars.

const h = Line(key,  { x1, y1, x2, y2, color, width=1, style="solid", pane })
          Box(key,   { x1, y1, x2, y2, fill, border, borderWidth=1, text, textColor, pane })
          Label(key, { x, y, text, color, size=12, align:"left"|"center"|"right",
                       anchor:"above"|"below"|"center", pane })
          Marker(key,{ x, y, shape, color, size, pane })
h.update({ ...partial })  // merge props in place
h.remove()                // delete the entity
// x is unix seconds, y is price (or sub-pane units when pane:"sub").
// Same key = upsert: you get the same handle back.

Alerts

Fire on the moment a condition becomes true. Alerts run during realtime only; warmup just arms the edge detector, so reloading history never spams old signals.

alert(id, condition, { message, cooldownSec=0, oncePerBar=true, once=false })
// Fires on a false -> true transition of condition, during realtime only.
// oncePerBar re-arms each new bar; once fires a single time until inputs change.
// Delivery: in-app toast + alert bell history + a background-tab notification.

Remote data

Pull an external time series and align it to the chart's bars. remote() fetches inside the worker, so scripts never touch fetch directly. It reads JSON or CSV and step-fills forward.

const fng = remote("https://api.alternative.me/fng/?limit=0", {
  path: "data",            // dot-path to the array in the JSON response
  time: "timestamp",       // field with unix time (sec or ms, auto-detected) or ISO string
  value: "value",          // field with the numeric value
  refreshSec: 3600,        // accepted, but polling is not active yet — fetches once per load
  format: "json"           // "json" | "csv" (csv: time/value are column names)
})
// -> a Series aligned to chart bars, step-forward filled. NaN before the first datapoint.

Works for CORS-enabled APIs. For blocked ones, relative URLs resolve against the site and hit the existing proxies (/api/okx-funding, /api/mexc, and more). Limits: 4 calls per script, 5 MB and 10 s each. Every remote URL is shown in the trust prompt before a shared script runs.

Limits

Guardrails that keep a bad script from taking the terminal down with it. A missing value is always NaN, never null. Most breaches are hard errors surfaced as a red badge on the legend with the line and column.

LimitValueOn breach
Inputs64hard error
Feed subscriptions12hard error
Plot series ids64hard error
Entities per type2,000evict oldest + one-time warning
Warmup budget10 sterminate, error state
Realtime batch budget2 sterminate, error state
Unresponsive worker15 sterminate, error state
Output payload16 MBerror state
remote() calls4 (5 MB, 10 s each)hard error
Script source size256 KBreject at load

Examples

Complete scripts, each copy-paste ready. Orderflow Signals, Tiered CVD, Prior Day Levels, Trading Sessions and VWAP bands also ship as templates in the add-script menu; the ones marked preview use feeds whose data sourcing lands in a future update. Tiered CVD needs footprint data, so on the historical chart its lines read NaN.

orderflow-signals.js
//@cx v1
study("Orderflow Signals", { pane: "overlay" })

input.group("Signals")
const showAbsorption = input.bool("Absorption", true)
const showExhaustion = input.bool("Exhaustion", true, { inline: true })
const showAggression = input.bool("Aggression", true)
const showDivergence = input.bool("Delta divergence", true, { inline: true })
const showConfluence = input.bool("Confluence", true)
const showMarkers    = input.bool("Signal markers", true, { inline: true })
const textMode       = input.select("Signal text", "Short", { options: ["Off", "Short", "Full"] })

input.group("Thresholds")
const normLength    = input.int("Normalization length", 75, { min: 25, max: 1000 })
const swingLookback = input.int("Swing lookback", 20, { min: 5, max: 200, inline: true })
const deltaStrengthMin = input.float("Delta strength", 3.0, { min: 0.5, max: 12 })
const volStrengthMin   = input.float("Volume strength", 2.2, { min: 0.5, max: 12, inline: true })
const absorptionEffMax = input.float("Absorption efficiency", 0.38, { min: 0.05, max: 0.9 })
const aggrClosePct     = input.float("Aggression close %", 0.72, { min: 0.5, max: 0.98, inline: true })
const exhaustionMax    = input.float("Exhaustion max strength", 1.25, { min: 0.1, max: 5 })
const confluenceMin    = input.int("Confluence count", 2, { min: 2, max: 4, inline: true })

input.group("Style")
const markerSize   = input.int("Marker size", 10, { min: 4, max: 30 })
const labelSize    = input.int("Text size", 13, { min: 8, max: 36, inline: true })
const markerOffset = input.float("Marker offset", 0.16, { min: 0.02, max: 1 })
const textOffset   = input.float("Text offset", 1.15, { min: 0.2, max: 4, inline: true })
const buyColor  = input.color("Buy", "#00c2ff")
const sellColor = input.color("Sell", "#ff4d6d", { inline: true })
const absColor  = input.color("Absorption", "#d7f2ff")
const exhColor  = input.color("Exhaustion", "#ffd166", { inline: true })
const divColor  = input.color("Divergence", "#ffffff")
const confColor = input.color("Confluence", "#ffe45e", { inline: true })

const bars = candles()
const vd   = volumeDelta()
const cvd  = cumulativeDelta()

const absDelta = vd.map(Math.abs)
const prevHigh = bars.high.shift(1)
const prevLow  = bars.low.shift(1)
const prevCvd  = cvd.shift(1)

onCandle((bar) => {
  const range = bars.high(0) - bars.low(0)
  const body  = Math.abs(bars.close(0) - bars.open(0))
  const atr   = ta.atr(bars.high, bars.low, bars.close, 14)
  const pad   = atr > 0 ? atr * markerOffset : chart.tickSize * 12
  const closePos   = range > 0 ? (bars.close(0) - bars.low(0)) / range : 0.5
  const efficiency = range > 0 ? body / range : 0
  const expansion  = atr > 0 ? range / atr : 0

  const volStdev   = ta.stdev(bars.volume, normLength)
  const deltaStdev = ta.stdev(absDelta, normLength)
  const volStrength   = volStdev > 0 ? bars.volume(0) / volStdev : NaN
  const deltaStrength = deltaStdev > 0 ? Math.abs(vd(0)) / deltaStdev : NaN

  const priorHigh    = ta.highest(prevHigh, swingLookback)
  const priorLow     = ta.lowest(prevLow, swingLookback)
  const priorCvdHigh = ta.highest(prevCvd, swingLookback)
  const priorCvdLow  = ta.lowest(prevCvd, swingLookback)

  const newHigh = bars.high(0) > priorHigh
  const newLow  = bars.low(0)  < priorLow
  const strongDelta = deltaStrength >= deltaStrengthMin
  const strongVol   = volStrength   >= volStrengthMin

  const bullAbsorption = showAbsorption && newLow  && vd(0) < 0 && strongDelta && strongVol
    && closePos >= 0.45 && efficiency <= absorptionEffMax
  const bearAbsorption = showAbsorption && newHigh && vd(0) > 0 && strongDelta && strongVol
    && closePos <= 0.55 && efficiency <= absorptionEffMax

  const bullExhaustion = showExhaustion && newLow  && deltaStrength <= exhaustionMax
    && volStrength <= exhaustionMax && closePos >= 0.35
  const bearExhaustion = showExhaustion && newHigh && deltaStrength <= exhaustionMax
    && volStrength <= exhaustionMax && closePos <= 0.65

  const buyAggression = showAggression && vd(0) > 0 && strongDelta && strongVol
    && closePos >= aggrClosePct && efficiency >= 0.45 && expansion >= 0.75
  const sellAggression = showAggression && vd(0) < 0 && strongDelta && strongVol
    && closePos <= 1 - aggrClosePct && efficiency >= 0.45 && expansion >= 0.75

  const bullDivergence = showDivergence && bars.low(0) < priorLow  && cvd(0) > priorCvdLow  && deltaStrength >= 1.0
  const bearDivergence = showDivergence && bars.high(0) > priorHigh && cvd(0) < priorCvdHigh && deltaStrength >= 1.0

  const bullScore = (bullAbsorption?1:0) + (bullExhaustion?1:0) + (buyAggression?1:0) + (bullDivergence?1:0)
  const bearScore = (bearAbsorption?1:0) + (bearExhaustion?1:0) + (sellAggression?1:0) + (bearDivergence?1:0)
  const bullConfluence = showConfluence && bullScore >= confluenceMin
  const bearConfluence = showConfluence && bearScore >= confluenceMin

  const yB1 = bars.low(0) - pad,        yA1 = bars.high(0) + pad
  const yB2 = bars.low(0) - pad * 1.65, yA2 = bars.high(0) + pad * 1.65
  const yB3 = bars.low(0) - pad * 2.3,  yA3 = bars.high(0) + pad * 2.3

  marker("bull_abs", showMarkers && bullAbsorption ? yB1 : NaN,
    { shape: "square", color: color.alpha(absColor, 0.9), size: markerSize, border: buyColor })
  marker("bear_abs", showMarkers && bearAbsorption ? yA1 : NaN,
    { shape: "square", color: color.alpha(absColor, 0.9), size: markerSize, border: sellColor })
  marker("bull_exh", showMarkers && bullExhaustion ? yB2 : NaN,
    { shape: "diamond", color: color.alpha(exhColor, 0.95), size: markerSize, border: buyColor })
  marker("bear_exh", showMarkers && bearExhaustion ? yA2 : NaN,
    { shape: "diamond", color: color.alpha(exhColor, 0.95), size: markerSize, border: sellColor })
  marker("buy_aggr", showMarkers && buyAggression ? yB3 : NaN,
    { shape: "triangle-up", color: buyColor, size: markerSize + 3 })
  marker("sell_aggr", showMarkers && sellAggression ? yA3 : NaN,
    { shape: "triangle-down", color: sellColor, size: markerSize + 3 })
  marker("bull_div", showMarkers && bullDivergence ? yB1 : NaN,
    { shape: "triangle-up", color: divColor, size: markerSize + 1, border: buyColor })
  marker("bear_div", showMarkers && bearDivergence ? yA1 : NaN,
    { shape: "triangle-down", color: divColor, size: markerSize + 1, border: sellColor })
  marker("bull_conf", showMarkers && bullConfluence ? yB3 : NaN,
    { shape: "asterisk", color: confColor, size: markerSize + 7 })
  marker("bear_conf", showMarkers && bearConfluence ? yA3 : NaN,
    { shape: "asterisk", color: confColor, size: markerSize + 7 })

  if (textMode !== "Off") {
    const t = (full, short) => textMode === "Full" ? full : short
    const put = (key, y, text, c, anchor) =>
      Label(`${key}_${bar.unix}`, { x: bar.unix, y, text, color: c, size: labelSize,
                                    align: "center", anchor })
    if (bullAbsorption) put("abs_b", yB1 - pad * textOffset, t("Absorption", "ABS"), absColor, "below")
    if (bearAbsorption) put("abs_s", yA1 + pad * textOffset, t("Absorption", "ABS"), absColor, "above")
    if (bullExhaustion) put("exh_b", yB2 - pad * textOffset, t("Exhaustion", "EXH"), exhColor, "below")
    if (bearExhaustion) put("exh_s", yA2 + pad * textOffset, t("Exhaustion", "EXH"), exhColor, "above")
    if (buyAggression)  put("agr_b", yB3 - pad * textOffset, t("Aggression", "AGR"), buyColor, "below")
    if (sellAggression) put("agr_s", yA3 + pad * textOffset, t("Aggression", "AGR"), sellColor, "above")
    if (bullDivergence) put("div_b", yB1 - pad * textOffset, t("Divergence", "DIV"), divColor, "below")
    if (bearDivergence) put("div_s", yA1 + pad * textOffset, t("Divergence", "DIV"), divColor, "above")
  }

  alert("bull_confluence", bullConfluence, {
    message: `Bull confluence (${bullScore}/4) @ ${fmt.price(bars.close(0))}`, cooldownSec: 60 })
  alert("bear_confluence", bearConfluence, {
    message: `Bear confluence (${bearScore}/4) @ ${fmt.price(bars.close(0))}`, cooldownSec: 60 })
})

Author with an LLM

Because CX Script is plain JavaScript with a small, documented surface, code assistants write it well. There is a machine-readable version of this entire API, condensed for pasting into a chat. Give it the spec, describe the indicator in a sentence or two, and paste what comes back into the editor. If it throws, the error carries the line and column.

Machine-readable spec

The full API and both flagship examples, as a single text file an assistant can read in one shot.