# CX Script v1 — API for LLMs You are writing a CX Script: a custom trading indicator for the Cryexc terminal. The language is plain JavaScript (ES2020, strict mode). Output ONE self-contained script. Do not add prose, imports, or markdown fences. Follow these rules exactly. ## Hard rules - First line MUST be exactly: //@cx v1 - Call study(name, opts?) exactly once, at top level. - Call onCandle(fn) exactly once, at top level. - Top level runs ONCE (declarations: study, input.*, feeds, onCandle registration). After it returns, inputs/feeds/study are frozen; calling them inside onCandle throws. - onCandle(fn) runs once per historical bar (warmup), then again for the live bar on every update. Scripts are BAR granularity, never per-trade. Live updates batch at up to 10 Hz, so onCandle fires many times on the forming bar — gate once-per-bar logic on bar.isNew. - The ONLY missing value is NaN. Never null/undefined. plot(id, NaN) = gap. marker(id, NaN) = no marker this bar. - No import/require. fetch/XMLHttpRequest/eval/Function are blocked (use remote()). - Strings in plot/label/box text should be ASCII only (the chart font atlas is U+0020-U+00FF; anything else renders as a missing glyph). - Colours are hex strings: "#RRGGBB" or "#RRGGBBAA". - One index convention: series accessor s(offset), 0 = current bar, 1 = one back. Out of range returns NaN. ## study study(name, { pane }) // pane: "overlay" (price chart) | "sub" (dedicated pane). default "overlay". ## bar object (arg of onCandle) bar.index absolute bar index, 0 = oldest loaded bar bar.unix bar open time, unix SECONDS (matches chart X axis) bar.isNew true the first time this bar is processed (gate once-per-bar logic on this) bar.isLast true if newest bar bar.isWarmup true during warmup, false for live updates ## chart (read-only global) chart.symbol, chart.venue, chart.intervalSec (0 if non-time), chart.aggMode ("time"|"tick"|"volume"|"range"|"delta"), chart.tickSize, chart.barCount, chart.upColor, chart.downColor (theme hex strings) ## inputs (top level only, max 64, each returns current value) 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) input.color(label, def, opts?) -> "#RRGGBB" | "#RRGGBBAA" input.string(label, def, opts?) -> string opts: secret (stripped from shares) input.group(label) -> undefined (section header) shared opts: key (persistence), inline:true (same row), showIf:{ key, eq|neq|in } ## series A Series is callable: s(offset=0) -> number. Helpers: s.map(fn) derived Series e.g. vd.map(Math.abs) s.shift(n) s'(i) = s(i+n) e.g. bars.high.shift(1) = prior highs s.length bars available series(fn) custom Series from offset fn: series(i => bars.high(i) - bars.low(i)) ## feeds (top level only, max 12; optional { venue }) candles() -> { open, high, low, close, volume, buyVolume, sellVolume, tradeCount, unix } (each a Series) volumeDelta(opts?) -> Series (per-bar delta). tiers only on footprint chart. cumulativeDelta(opts?) -> Series (running sum). tiers only on footprint chart. footprintLevels() -> .levels(offset) -> [{price,bid,ask,trades}], .poc(offset) -> price (FOOTPRINT ONLY) orderbook() -> { bestBid, bestAsk, mid, spread } Series + bidDepth(pct,offset), askDepth(pct,offset), imbalance(pct,offset); pct in {0.5, 2, 10} openInterest() -> { open, high, low, close } funding() -> { rate, nextFundingTime } (step series) liquidations() -> { longVol, shortVol, longQty, shortQty } bigTrades(opts?) -> .list(offset) -> [{unix, price, qty, notional, side}] remote(url, opts) -> bar-aligned Series (see remote section) tiers (footprint only): volumeDelta({ tiers: [6,7] }) or { tiers: "small"|"mid"|"whale" } small = < $100K, mid = $100K-$1M, whale = >= $1M DATA AVAILABILITY (current build) — respect this or the indicator renders empty: - LIVE: candles, volumeDelta, cumulativeDelta, openInterest, liquidations, footprintLevels. - NOT SOURCED YET (valid API, returns NaN / empty; scripts still load and run): orderbook, funding, bigTrades. - Scripts render on the FOOTPRINT chart only. Historical-chart rendering is a future update. Feed semantics you must not get wrong: - openInterest values are in COIN/base units (e.g. BTC), never USD notional. The default venue is binancef; bybitf and hyperliquidf are also available via { venue }. Any other venue yields a NaN series (never an error). The OI venue is INDEPENDENT of the chart's venue — a spot chart still reads perp OI, exactly like the OI subplot. - liquidations is one primary-symbol stream: { venue } is accepted and IGNORED. A bar inside the retained window with no liquidations is a genuine 0; a bar older than the oldest retained event is NaN (never assume 0 means "no liquidations happened"). - footprintLevels carries per-price rows for the most recent ~3,000 bars. Older bars return an empty list and poc() = NaN. - History depth varies by source and retention: liquidations ~8 days, open interest ~130k snapshots. Beyond that, NaN. Nothing is ever fabricated. HISTORY SOURCE (Settings -> History -> Source) changes what the warmup bars contain: - "Full history" (hosted archive, alpha): the last 48 HOURS of real footprint history on every covered symbol. Those bars have open/high/low/close, volume, delta and footprintLevels — but NO per-trade detail. On archive bars tradeCount is 0 and every TIERED read is 0: volumeDelta({tiers}) and cumulativeDelta({tiers}) stay flat across history and only start moving on the session's live bars. Plain volumeDelta() and cumulativeDelta() are correct across the whole 48h window. - "Live exchange" (default): no history stream. BTC gets a short history prefix that DOES carry tiers and tradeCount; every other symbol starts at the session's first bar, so the script warms up over a short window (ta periods longer than it return NaN). - Practical rule: if a script must work over history, prefer plain delta/CVD and avoid tradeCount. Reach for tiers only when the user is watching bars build live, and say so. ## ta library (~36 fns; take a Series, return a number at current bar unless noted -> object) 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 The ta.* names follow Pine Script conventions on purpose. A period longer than available history returns NaN (no partial windows). NaN propagates. ema/rma (and rsi/atr built on them) are SMA-seeded over the loaded window; earliest bars can differ slightly from TradingView (which recurses over its full history) and converge. A NaN that enters an ema/rma window persists in the recursive state — clean inputs before smoothing. ## fmt / color fmt.price(v) tick-size-aware decimals fmt.compact(v) "1.24M" fmt.pct(v, dp=1) takes a RATIO: 0.0172 renders as "1.7%" color.rgb(r,g,b,a=1) color.alpha(c,a) // a in [0,1], NOT transparency % color.lighten(c,pct) color.darken(c,pct) color.mix(a,b,t) color.gradient(value, min, max, colorLo, colorHi) ## plotting (call inside onCandle only; max 64 series ids; value NaN = gap) 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 }) shapes: circle square diamond triangle-up triangle-down cross plus asterisk arrow-up arrow-down plotCandle(id, o, h, l, c, { up, down, pane }) bg(colorOrNaN) // bar background tint fill(idA, idB, { color }) // both series must share a pane First call for an id fixes its pane ("overlay" | "sub"). ## entities (keyed drawings; upsert by key; call inside onCandle; x = unix seconds, y = price) 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 }) h.remove() Same key = update + same handle. Cap 2,000 per type (oldest evicted). ## alerts alert(id, condition, { message, cooldownSec=0, oncePerBar=true, once=false }) Fires on false->true transition, realtime only (warmup only arms the detector). ## remote (external data) const s = remote(url, { path: "data", // dot-path to the array in the JSON time: "timestamp", // field with unix time (sec/ms auto) or ISO string value: "value", // numeric field refreshSec: 3600, // accepted, but polling not active yet — fetches once per load format: "json" // "json" | "csv" (csv: time/value are column names) }) Returns a Series aligned to bars (step-forward fill; NaN before first datapoint). Max 4 remote() per script, 5 MB, 10 s each. Relative URLs hit site proxies (/api/*). ## limits (breach -> hard error unless noted) inputs 64 | feed subscriptions 12 | plot ids 64 | entities 2,000/type (evict oldest) | warmup 10 s (terminate) | realtime batch 2 s (terminate) | unresponsive worker 15 s (terminate) | output 16 MB (error) | remote() 4 (5 MB, 10 s each) | source 256 KB (reject) log(...args) prints to console (rate-limited 20/s). Each script runs in its own Web Worker: a runaway loop is killed, never the chart. ## misc helpers you can rely on Plain JS is available: Math, JSON, Array, closures, module-level let state. --- ## Example A — sub-pane plot with an input and TA //@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) 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 }) }) ## Example B — keyed entities on the price chart (prior-day levels) //@cx v1 study("Prior Day Levels", { pane: "overlay" }) const bars = candles() let dayHigh = NaN, dayLow = NaN, dayStart = 0 let prevHigh = NaN, prevLow = NaN onCandle((bar) => { if (!bar.isNew) return if (Math.floor(bar.unix / 86400) !== Math.floor(dayStart / 86400)) { prevHigh = dayHigh; prevLow = dayLow dayHigh = bars.high(0); dayLow = bars.low(0); dayStart = bar.unix } else { dayHigh = Math.max(dayHigh, bars.high(0)); dayLow = Math.min(dayLow, bars.low(0)) } if (!Number.isNaN(prevHigh)) { Line("pdh", { x1: dayStart, y1: prevHigh, x2: bar.unix, y2: prevHigh, color: "#e9c46a", style: "dashed" }) Line("pdl", { x1: dayStart, y1: prevLow, x2: bar.unix, y2: prevLow, color: "#e9c46a", style: "dashed" }) Label("pdh_l", { x: bar.unix, y: prevHigh, text: "PDH " + fmt.price(prevHigh), color: "#e9c46a", anchor: "above", align: "right" }) Label("pdl_l", { x: bar.unix, y: prevLow, text: "PDL " + fmt.price(prevLow), color: "#e9c46a", anchor: "below", align: "right" }) } })