MobChartDocs
Open MobChart

API for custom indicators

Write your own indicators in JavaScript, run them in a secure sandbox next to the chart and share them with other users. The API gives you market data (candles, order books, clusters, liquidations, derivatives data), plotting primitives, technical analysis helpers and alerts.

Generate indicators with AI

The whole API is also available as a single text file for language models: AI-scripting.txt. Attach it to ChatGPT, Claude, Gemini or any other LLM and describe the indicator you want.

Example prompt:

Read the attached [AI-scripting.txt] https://mobchart.com/docs/public/AI-scripting.txt file and use it as the only reference for the MobChart scripting API.
Create a custom indicator that plots a 20-period EMA and a 50-period EMA on the chart. Color the candles green when EMA20 is above EMA50 and red otherwise.
Use only the documented API. Do not invent functions. Return the complete indicator code.

Sandbox and limits

Indicator Development

Indicator development is done within a separate widget, which can be placed next to the chart where the indicator will be enabled.

Creating an Indicator

define(title, description, type, isPublic, isPublicCode, options)

Registers the indicator in the system.

ParameterTypeDescription
titlestringIndicator name
descriptionstringShort description
type'onchart' | 'offchart'Indicator placement (on chart or in a separate panel)
isPublictrue | falseWhether the indicator is public
isPublicCodetrue | falseWhether the source code is public
optionsobject (optional){ fullRange, warmup }, see below

Example:

define("My Indicator", "Shows EMA line", "onchart", true, false);

Calculation range. By default a script computes only the part of the chart that needs it: the bars loaded by scrolling back, or the latest bars on a refresh, plus warmup bars of history before them (default 500) that are needed by the calculations but not returned. Declare warmup when your indicator needs more or less history, e.g. { warmup: 200 } for an EMA(200). A script whose values or shapes depend on the whole visible range (VWAP from the first visible candle, cumulative delta, a label at the window maximum, levels drawn across the chart) must declare { fullRange: true }, otherwise its results are wrong after loading more history. The mode is read from define() every time the script is saved: a script saved without fullRange is calculated partially, so add the flag to existing cumulative scripts when you edit them. Partial scripts are also loaded in pieces: when you switch one on over a long history the chart requests the newest 500 candles first (or warmup candles when that is more) and the older parts follow one request at a time, so the visible part appears at once. Scripts with fullRange are always computed in a single request over the whole range.

define("Window max", "Label at the highest visible high", "onchart", true, true, { fullRange: true });

Indicator placement for onchart and offchart:

Input Parameters

The input and style classes allow you to create user-configurable settings (similar to input() in PineScript).

MethodDescription
number(title, defaultValue, min, max, step)Numeric input
range(title, defaultValue, min, max, step)Range input
select(title, defaultValue, values)Dropdown selection
color(title, defaultValue)Color picker
boolean(title, defaultValue)Boolean input
tickers(title, defaultValue, exchange)Multi select of the tickers of an exchange (default: the chart exchange); returns the chosen tickers as an array

Example (input) for main parameters:

const length = input.number("Period", 14, 1, 100, 1);
const type = input.select("Type", 'sma', ['sma', 'ema']);
const color = input.color("Line Color", "#FF0000");
// pairs to include / exclude, picked from the tickers of the chart exchange
const include = input.tickers("Include pairs", []);
const exclude = input.tickers("Exclude pairs", []);

Or (style) for visual parameters:

const length = style.number("Size", 14, 1, 100, 1);
const color = style.select("Background Color", '#FF0000', ["#FF0000", "#AA0000", "#BB0000"]);
const color = style.color("Background Color", "#FF0000");

Data Sources

The source class fetches market data from the server. The candle-based methods take the same parameters; the live snapshots differ: orderbook() takes an exchange and a ticker, levels() and keyLevels() an exchange and a settings object.

ParameterDescription
exchange, ticker, timeframeDefault to the chart the indicator is opened on. Pass other values to fetch data of another market.
lookbackNumber of bars of history from the current date, no more than 5000.

ohlcv(exchange, ticker, timeframe, lookback?)

Candle data.

[
{
"open": 115886.17,
"high": 115911.96,
"low": 115764.27,
"close": 115815.16,
"volume": 228.41528,
"startTime": 1757699100000
},
{...},
...
]

depths(exchange, ticker, timeframe, lookback?)

Historical order book snapshots, one per candle. Only the biggest limit orders are stored (the data behind the heatmap).

[
{
"data": {
"asks": {
"109425": 5.467,
"109495": 3.564,
...
},
"bids": {
"33000": 3.746,
"35000": 16.49,
...
}
},
"startTime": 1757699100000
},
{...},
...
]

clusters(exchange, ticker, timeframe, lookback?)

Cluster (footprint) data: buy and sell volume at every price level of the candle.

[
{
"data": {
"totalBuy": 16053385,
"totalSell": 20490989,
"0.8654": { "buy": 286845, "sell": 336689 },
"0.8653": { "buy": 271848, "sell": 259167 },
"0.8652": { "buy": 318814, "sell": 373497 },
...
},
"startTime": 1757699100000
},
{...},
...
]

liquidations(exchange, ticker, timeframe, lookback?)

Liquidated volume at every price level of the candle (futures markets only). Same format as clusters().

[
{
"data": {
"totalBuy": 16053385,
"totalSell": 20490989,
"0.8654": { "buy": 286845, "sell": 336689 },
"0.8653": { "buy": 271848, "sell": 259167 },
"0.8652": { "buy": 318814, "sell": 373497 },
...
},
"startTime": 1757699100000
},
{...},
...
]

moreData(exchange, ticker, timeframe, lookback?)

Derivatives data per candle: funding rate, open interest candle, long/short ratios (futures markets only).

[
{
"data": {
"FR": 0.00003777, // Funding Rate
"OI": { // Open Interest Candle
"open": 85645.708,
"close": 83496.225,
"high": 85801.04,
"low": 83397.918
},
"LS": { "l": 0.5426, "s": 0.4574 }, // Long/Short Ratio
"taLS": { "l": 0.552, "s": 0.448 }, // Top Traders Account Ratio
"tpLS": { "l": 0.5996, "s": 0.4004 } // Top Traders Position Ratio
},
"startTime": 1757699100000
},
{...},
...
]

orderbook(exchange, ticker)

Live order book snapshot: the current full book, not tied to candles.

{
"asks": {
"109425": 5.467,
"109495": 3.564,
...
},
"bids": {
"109400": 1.2,
"109390": 0.8,
...
}
}

The levels are not sorted, use the ta orderbook helpers (ta.bestBid, ta.sumBids, ...) to work with the book. The historical books of the candles come from depths(): depths()[i].data has the same format, but only the biggest limit orders are stored there.

Example:

const candles = source.ohlcv(); // The exchange, ticker, and timeframe parameters default to the current chart.
const closes = candles.map(c => c.close);

levels(exchange, settings?)

The levels of the Liquidity screener: the big resting limit orders of an exchange, filtered with the same settings as the screener page. Live data, not tied to candles. Without a plan the nearness is limited to 1%, the lifespan to 60 minutes and the result to 200 levels; with a plan up to 2000 levels. The largest orders come first. Counts as one data source request.

{
"BTCUSDT": {
"asks": {
"109500": {
"price": 109500,
"quantity": 42.5, // size in the base asset
"quantityUSDT": 4653750, // size in the quote currency
"distance": 0.62, // from the current price, percent
"duration": 184, // minutes the order has been standing
"touches": 2,
"currentTouches": 1,
"filledPercent": 12.4,
"strength": 95.5, // percent, see the screener
"diversity": 11,
"isContinuous": true,
"date": "2026-10-10T06:12:03.000Z"
},
...
},
"bids": { ... },
"volume1hTotal": 183000000
},
...
}

Settings (all optional):

SettingDefaultDescription
tickersall tickersonly these tickers of the exchange
exclude[]tickers to skip
type"all""all", "bids" or "asks"
nearness5max distance from the current price, percent
size100000min order size in the quote currency
lifespan0min minutes the order has been standing
touches, currentTouches0min touches of the level / of the current order
filled0min filled part of the order, percent
strength, diversity0min strength (percent) and diversity, as on the screener
continuousOnlyfalseskip orders that disappeared and came back
volume1h0min 1h volume of the ticker in the quote currency
spotAndFuturesfalseonly tickers that exist on both the spot and the futures market
limit500max number of levels, the largest first (max 2000)

Example:

// the big orders around the price of the chart market as horizontal lines, labelled with the size and the lifespan
const candles = await source.ohlcv();
const lastDate = candles.at(-1).startTime;
const levels = await source.levels(currentExchange, { tickers: [currentTicker], nearness: 3, size: 300000, lifespan: 30 });
const own = levels[currentTicker] || { asks: {}, bids: {} };
for (const [type, color] of [["asks", "#ef5350"], ["bids", "#26a69a"]]) {
for (const [price, level] of Object.entries(own[type])) {
plot.horizontalLine(`${type} ${price}`, +price, color, 0.8, Math.min(4, 1 + level.quantityUSDT / 1e6));
plot.text(`${type} ${price} label`, `${(level.quantityUSDT / 1e6).toFixed(2)}M · ${level.duration}m`, lastDate, +price, 10, color);
}
}

keyLevels(exchange, settings?)

The levels of the Key levels screener: open, high and low of the current and the previous day, week, month and year of every ticker of an exchange, with the distance of each level from the current price. Live data, not tied to candles. Without a plan the result holds only the levels within ±1% of the price (the fixed range of the screener page) and at most 100 tickers; with a plan any distance range and up to 1000 tickers. The tickers with the nearest level come first. Counts as one data source request.

{
"BTCUSDT": {
"price": 79776.57, // current price
"updatedAt": 1788707880000,
"bootstrapped": true, // false while the history of the ticker is still being collected
"nearest": 0.08, // distance of the nearest level, percent
"matches": 5, // levels inside the distance range
"daily": { "start": 1788652800000, "open": 79700, "high": 79985, "low": 79710, "openApprox": false,
"distance": { "open": -0.1, "high": 0.26, "low": -0.08 } }, // percent from the price, signed
"prevDay": { ... }, "weekly": { ... }, "prevWeek": { ... },
"monthly": { ... }, "prevMonth": { ... }, "yearly": { ... }, "prevYear": null // null: no data for the period yet
},
...
}

Settings (all optional):

SettingDefaultDescription
tickersall tickersonly these tickers of the exchange
exclude[]tickers to skip
periodsall"daily", "prevDay", "weekly", "prevWeek", "monthly", "prevMonth", "yearly", "prevYear"
fieldsall"open", "high", "low": the levels to return
distanceFrom, distanceTononesigned distance range from the price, percent (e.g. -2 and 2); only tickers with a level inside the range are returned. Without a plan the range is fixed at ±1%
limit200max number of tickers, the nearest level first (max 1000; 100 without a plan)

Example:

// the key levels of the chart market as horizontal lines
const candles = await source.ohlcv();
const lastDate = candles.at(-1).startTime;
const rows = await source.keyLevels(currentExchange, { tickers: [currentTicker], periods: ["daily", "prevDay", "weekly"] });
const own = rows[currentTicker];
if (own) {
for (const period of ["daily", "prevDay", "weekly"]) {
const level = own[period];
if (!level) continue;
for (const [field, color] of [["high", "#ef5350"], ["low", "#26a69a"], ["open", "#90a4ae"]]) {
if (level[field] == null) continue;
plot.horizontalLine(`${period} ${field}`, level[field], color, 0.8, 1);
plot.text(`${period} ${field} label`, `${period} ${field.toUpperCase()}`, lastDate, level[field], 10, color);
}
}
}

levelsMetrics(exchange, settings?)

The history of the strong liquidity levels collected for the Metrics page: every 5 minutes the server snapshots the liquidity screener levels of every exchange with a duration of at least 30 minutes and a strength of at least 80% and keeps a week of them. The source filters the snapshots of a period like the page does and returns, per snapshot, the number and the $ size of the matching levels, the aggregates per ticker and the lifecycle of every level. Collected data, not tied to candles: match series[].at with the candle dates. Without a plan the period is at most 24 hours and the list holds at most 100 levels; with a plan up to 7 days and 1000 levels. Counts as one data source request.

{
"exchange": "binanceFutures",
"hours": 24,
"snapshotMinutes": 5,
"latestAt": 1792102098685,
"summary": { "snapshots": 287, "uniqueLevels": 217, "uniqueTickers": 118, "activeNow": 180, "bidsNow": 102, "asksNow": 78, "usdNow": 119294436 },
"series": [ // one point per snapshot, oldest first
{ "at": 1792015822555, "bids": 24, "asks": 21, "bidsUsd": 1851345, "asksUsd": 1228883, "tickers": 20 },
...
],
"tickers": { // per ticker, over the period
"BTCUSDT": { "now": 3, "nowUsd": 95710480, "snapshots": 63, "bids": 3, "asks": 0, "levels": 3,
"maxUsd": 42347880, "maxDuration": 8220, "maxStrength": 100 },
...
},
"levels": [ // the lifecycle of every level, active ones first
{ "ticker": "BTCUSDT", "side": "bids", "price": 52050, "usd": 42347880, "quantity": 813.6, "distance": 19.8,
"duration": 8220, "strength": 100, "continuous": 1, "firstSeen": 1792015822555, "lastSeen": 1792102098685,
"snapshots": 63, "maxDuration": 8220, "maxStrength": 100, "active": true },
...
],
"levelsTotal": 217
}

Settings (all optional):

SettingDefaultDescription
tickersall tickersonly these tickers of the exchange
exclude[]tickers to skip
hours24period, hours (max 168; 24 without a plan)
minDuration30min order duration, minutes: 30, 60, 120 or 180
minStrength80min order strength, percent: 80, 90 or 100
maxDistancenonemax distance of the order to the current price, percent, checked in every snapshot
stableOnlyfalsetrue: only orders whose quantity never jumped ("exclude orders with unstable quantities")
limit200max number of levels in levels (max 1000; 100 without a plan)

Example:

// $ size of the strong levels of the chart market per candle
const candles = await source.ohlcv();
const data = await source.levelsMetrics(currentExchange, { tickers: [currentTicker], hours: 24, minDuration: 60, minStrength: 90, stableOnly: true });
const series = data.series; // oldest first
let j = 0;
const buy = [], sell = [];
for (let i = 0; i < candles.length; i++) {
const end = candles[i + 1] ? candles[i + 1].startTime : candles[i].startTime + (candles[1].startTime - candles[0].startTime);
while (j + 1 < series.length && series[j + 1].at < end) j++; // the last snapshot inside the candle
const point = series[j] && series[j].at < end ? series[j] : null;
buy.push(point ? point.bidsUsd : null);
sell.push(point ? point.asksUsd : null);
}
plot.areaSeries("Buy levels ($)", buy, "#26a69a", 0.3, "#26a69a", 1, 1, 0);
plot.areaSeries("Sell levels ($)", sell, "#ef5350", 0.3, "#ef5350", 1, 1, 0);

HTTP Requests

The http class sends requests to external HTTP APIs and returns the response, so an indicator can use data that is not available in source: your own server, a public REST API, an on-chain metrics provider, ... Like source.*, the calls are written as synchronous functions, no await is needed.

MethodDescription
get(url, options?)GET request
post(url, body?, options?)POST request. An object or array body is sent as JSON (Content-Type: application/json), a string is sent as is

options is { headers: { name: value } } — up to 10 custom headers (for example Authorization or x-api-key).

Both methods return the same response object and never throw:

{
ok: true, // status 200–299
status: 200, // HTTP status, 0 when the request was not sent
headers: { "content-type": "application/json", ... },
data: { ... }, // response body parsed as JSON, null when the body is not JSON
text: "...", // raw response body
error: null // reason when the request failed: "timeout", "url not allowed", "too many requests", ...
}

Possible error values: invalid url, url not allowed, invalid headers, invalid body, body too large, response too large, timeout, too many requests, rate limit, busy, proxy unavailable, network error.

Example (GET):

define("Fear & Greed", "Crypto Fear & Greed index", "offchart", true, true);
const res = http.get("https://api.alternative.me/fng/?limit=30");
if (!res.ok) logger.log("request failed:", res.status, res.error);
// res.data = { data: [{ value: "71", timestamp: "1757635200" }, ...] }
const byDay = {};
(res.data?.data || []).forEach((row) => { byDay[Number(row.timestamp) * 1000] = Number(row.value); });
const candles = source.ohlcv();
const values = candles.map((c) => c ? byDay[c.startTime - (c.startTime % 86400000)] ?? null : null);
plot.lineSeries("Fear & Greed", values, "#00C8FF");

Example (POST):

const candles = source.ohlcv();
const last = candles.at(-1);
const res = http.post(
"https://example.com/api/signals",
{ exchange: currentExchange, ticker: currentTicker, close: last.close },
{ headers: { "x-api-key": "YOUR_KEY" } } // private code only
);
logger.log("signal sent:", res.ok, res.status, res.data);

Global Variables

VariableDescriptionExample
currentExchangeCurrent exchange on the chartbinance, binanceFutures, bybit, bybitFutures, okx, okxFutures, coinbase, bitfinex, bitfinexFutures, hyperliquid, hyperliquidFutures, asterFutures
currentTickerCurrent ticker on the chart (format depends on the exchange)BTCUSDT, ADA-USD, BTC/USDC, ...
currentTimeframeCurrent timeframe on the chart1m, 5m, 15m, ...
exchangesNames of every exchange (frozen array)["binance", "binanceFutures", ...]
tickersTickers of every exchange: tickers[currentExchange] is the list of the chart exchange (frozen)tickers.binance = ["BTCUSDT", "ETHUSDT", ...]
heatmapStepPrice step of the order book heatmap and clusters at the current price (the precision the server rounds prices to: 5 above 5000, 1 for 1000-5000, 0.1 for 100-1000, ...), taken from the latest close of the chart; null when the price is not known yet (known after the first source.ohlcv() of the chart market)5, 1, 0.1, 0.001, ...

Example:

const candles = source.ohlcv(currentExchange, currentTicker, currentTimeframe);
const closes = candles.map(c => c.close);

Ticker formats. Every source method that takes an exchange and a ticker runs the ticker through resolveTicker: the base coin is taken from whatever format you pass (ETHUSDT, ETH-USD, ETH/USDC, ETH:USD, ETHF0:USTF0, ...) and the ticker is rebuilt in the format of the exchange you ask for. So ETH-USD works for Binance and currentTicker of a Binance chart works for Coinbase. Only the coin is kept: the exchange's standard quote pair is used (USDT on Binance, USD on Coinbase, USDC on Hyperliquid, ...), and a coin the exchange does not list gives an empty result with a line in the logs.

// a Coinbase-style ticker on Binance Futures: resolved to ETHUSDT
const depths = source.depths("binanceFutures", "ETH-USD");
// the ticker of a Binance chart on Coinbase: ETHUSDT becomes ETH-USD
const spot = source.ohlcv("coinbase", currentTicker, currentTimeframe);

Logging

The logger class is used for outputting information and debugging.

MethodDescription
log(...args)Adds an entry to the log (with limits on length and quantity)

Example:

const candles = source.ohlcv();
logger.log("First candle:", candles?.[0]);

Visualization

The plot class is responsible for drawing graphical elements and data series.

Geometric Shapes

MethodDescription
box(title, date1, price1, date2, price2, fill, opacity, stroke, strokeOpacity, strokeWidth)Rectangle
square(title, date1, price1, size, fill, opacity, stroke, strokeOpacity, strokeWidth)Square
diamond(title, date1, price1, size, fill, opacity, stroke, strokeOpacity, strokeWidth)Diamond
triangle(title, date1, price1, size, fill, opacity, stroke, strokeOpacity, strokeWidth)Triangle
circle(title, date1, price1, radius = 10, fill, opacity, stroke, strokeOpacity, strokeWidth)Circle
text(title, value, date1, price1, fontSize, fill, opacity)Text on chart
table(title, rows, options)Table pinned to a corner of the chart, see Tables
straightLine(title, date1, price1, date2, price2, stroke, strokeOpacity, strokeWidth, strokeDasharray)Straight line
horizontalLine(title, price1, stroke, strokeOpacity, strokeWidth, strokeDasharray)Horizontal line across the whole chart
verticalLine(title, date1, stroke, strokeOpacity, strokeWidth, strokeDasharray)Vertical line across the whole chart
ParameterData TypeDescription
titlestringElement name (unique identifier on the chart)
date1number (timestamp, ms)Starting timestamp
price1numberStarting price (Y-axis coordinate)
date2number (timestamp, ms)Ending timestamp (for lines and rectangles)
price2numberEnding price (for lines and rectangles)
sizenumberShape size (in pixels)
radiusnumberCircle radius (default 10)
fillstring (hex, e.g., "#FF0000")Fill color of the shape
opacitynumber (0–1)Fill opacity
strokestring (hex, e.g., "#000000")Stroke color
strokeOpacitynumber (0–1)Stroke opacity
strokeWidthnumberStroke width (in pixels)
strokeDasharrayarray (e.g., [5,5])Dashed line style
fontSizenumberText size (in pixels)
valuestringText value (for text method)

Example:

plot.text("Text object", "Hello world!", 1757699100000, 115000, 10, "#00C8FF", 0.5);
plot.straightLine(`${namePrefix} Open`, level.timeStart, level.open, level.timeEnd, level.open, color);
plot.circle(`${namePrefix} High`, level.timeStart, level.high, 3, color);

No more than 3000 shapes can be displayed on the chart.

Data Series

MethodDescription
lineSeries(title, values, stroke, strokeOpacity, strokeWidth, strokeDasharray)Line chart
barSeries(title, values, fill, opacity)Bar chart
candleSeries(title, openValues, closeValues, highValues, lowValues, fill, opacity)Candles
areaSeries(title, values, fill, opacity, stroke, strokeOpacity, strokeWidth, base)Filled area between the line and base (default: the bottom of the chart)
bandSeries(title, upperValues, lowerValues, fill, opacity, stroke, strokeOpacity, strokeWidth, step)Filled band between two series, every candle keeps its own color and opacity
profile(title, values, options)Volume-profile style histogram of the visible candles, see Profiles
histogram(title, rows, options)A finished price map drawn as a profile, see Histograms
ParameterData TypeDescription
values, upperValues, lowerValues(number \| null)[]One value per candle, null hides the candle
fill, strokestring (hex) or functionColor, or a function returning the color of the candle: (value, i) for lineSeries, barSeries, candleSeries and areaSeries, (upper, lower, i) for bandSeries
opacitynumber (0–1) or functionFill opacity, a function (value, i) / (upper, lower, i) for areaSeries / bandSeries
basenumber | nullareaSeries base level, null = the bottom of the chart
stepbooleanbandSeries style: true = one column per candle (default), false = a continuous polygon

Example:

plot.lineSeries("EMA", ta.ema(closes, 20), "#00C8FF");
// every candle keeps its own color: green where the close is above the EMA, red below it
plot.lineSeries("Close", closes, (v, i) => v > ema[i] ? colorBuy : colorSell);
plot.barSeries("Premium %", premiumPercents, ((d) => d > 0 ? color1 : color2), 1);
plot.candleSeries(
"Candles",
opens, closes, highs, lows,
(v, i) => {
return opens?.[i] > closes?.[i] ? colorBuy : colorSell
}
);
plot.areaSeries("Volume", volumes, "#00C8FF", 0.3);
// area colored per candle, more opaque on the bigger candles
plot.areaSeries("Delta", deltas, (v, i) => v >= 0 ? colorBuy : colorSell, (v, i) => Math.min(1, Math.abs(v) / maxDelta));
// pressure band between the close and 1% below it, colored and shaded by the imbalance ratio of the candle
plot.bandSeries(
"Bid pressure",
closes,
closes.map(c => c * 0.99),
(upper, lower, i) => ratios[i] > 0 ? bidColor : askColor,
(upper, lower, i) => Math.abs(ratios[i]) / 100
);

No more than 20 data series can be displayed on the chart.

Profiles

plot.profile(title, values, options?) draws a histogram by price, like the built-in volume profile. values[i] is the price map of candle i of ohlcv(): { price: value } or { price: { buy, sell } } (the data of a clusters() or liquidations() record can be passed as is), or null for a candle without rows. The rows of the candles visible on the chart are summed per price step on the chart itself, so the profile follows pan, zoom and load more without a new run. Single values are drawn in one colour (a negative value goes to fill2), pairs split the bar into buy and sell parts and allow delta mode. profile() returns no alert handle.

OptionDefaultDescription
stepchart tick sizerow height in price units, prices are snapped down to it
orient"right"edge the bars grow from, "right" or "left"
width30widest row as a percent of the chart width
fill"#26a69a"colour of the value, or of the buy part
fill2"#ef5350"colour of the sell part
opacity0.6bar opacity
deltafalsedraw buy - sell instead of the sum, pairs only
poctrueline at the row with the largest value
valueArea70percent of the total around the POC drawn at full opacity, false to disable
valueAreaOpacity0.3opacity of the rows outside the value area
labelstruerow totals on the bars when there is room

Limits: 3 profiles per run, 300 rows per candle, 200000 rows per profile. Rows beyond the limits are dropped and the run logs a note.

Example:

// volume profile of the chart, the same picture as the built-in tool
const rows = await source.clusters();
plot.profile("Volume", rows.map((r) => r?.data ?? null), { step: 5 });
// only large trades, delta coloured, on the left edge
const minSize = input.number("Min trade size", 50, 0, 100000, 1);
const big = rows.map((r) => {
if (!r?.data) return null;
const out = {};
for (const [price, c] of Object.entries(r.data)) {
if (price === "totalBuy" || price === "totalSell") continue;
if (c.buy + c.sell >= minSize) out[price] = { buy: c.buy, sell: c.sell };
}
return out;
});
plot.profile("Large trades", big, { step: 5, delta: true, orient: "left" });

Histograms

plot.histogram(title, rows, options?) draws a histogram you have already built: one price map for the whole chart, nothing is summed from the candles. Use profile() when the rows belong to candles and the picture should follow the visible range, and histogram() when the rows are final: an order book snapshot, levels the script computed, data of another market.

rows has the same formats as a candle entry of profile(): { price: value }, { price: { buy, sell } } or a list of [price, buy, sell] rows. A single value is drawn in one colour (a negative value goes to fill2), pairs split the bar into buy and sell parts and allow delta mode. The options are the same as for profile(), see the table above.

The histogram is dated with the last candle of the run and the chart keeps the newest histogram of a title, so partial runs over older candles (loading history) never replace the current one. It is redrawn with every refresh of the script, so an order book histogram follows the live book. histogram() returns no alert handle.

Limits: 3 histograms per run, 5000 rows. Rows beyond the limit are dropped and the run logs a note.

Example, the order book:

// the live order book as a profile on the right edge: bids in the buy colour, asks in the sell colour
const book = await source.orderbook();
const rows = {};
for (const [price, qty] of Object.entries(book.bids)) rows[price] = { buy: qty };
for (const [price, qty] of Object.entries(book.asks)) rows[price] = { sell: qty };
plot.histogram("Order book", rows, { step: 5, width: 25, valueArea: false, poc: false });

Example, your own levels:

// your own histogram: cluster volume of the last 100 candles, recent candles weigh more
const rows = await source.clusters();
const weighted = {};
rows.slice(-100).forEach((r, i, arr) => {
if (!r?.data) return;
const weight = (i + 1) / arr.length;
for (const [price, c] of Object.entries(r.data)) {
if (price === "totalBuy" || price === "totalSell") continue;
const cell = weighted[price] || (weighted[price] = { buy: 0, sell: 0 });
cell.buy += (c.buy || 0) * weight;
cell.sell += (c.sell || 0) * weight;
}
});
plot.histogram("Weighted volume", weighted, { step: 5, orient: "left", delta: true });

Tables

plot.table(title, rows, options?) draws a small grid pinned to a corner of the chart pane: multi-timeframe summaries, statistics of the loaded range, signal dashboards. rows is an array of rows, a row is an array of cells, and a cell is a string, a number or { text, color, bg, align, bold }. Tables of the same corner stack in order. A table is dated with the last candle of the run and the chart keeps the newest table of a title, so partial runs over older candles (loading history) never replace the current one. Tables are not drawn on charts narrower than 500 px. A table can be dragged with the mouse: the position is saved with the workspace and reset when the script changes its position option. table() returns no alert handle.

OptionDefaultDescription
position"top-right"corner of the pane: "top-right", "top-left", "bottom-right", "bottom-left"
headerfalsefirst row drawn bold on a shaded band
fontSize11text size in px, 8 to 16
color, bg, borderby themedefault text, panel and grid colours; a cell can override color and bg
opacity0.85panel opacity
align"left"default cell alignment, "left", "center" or "right"

Limits: 3 tables per run, 50 rows, 10 columns, 100 characters per cell. Longer text is cut with an ellipsis, a column takes at most 30% of the chart width.

Example:

const candles = await source.ohlcv();
const closes = candles.map((c) => c.close);
const rsi = ta.rsi(closes, 14);
const last = rsi.at(-1);
const change = (closes.at(-1) / closes.at(-25) - 1) * 100;
plot.table("Stats", [
["Metric", "Value"],
["RSI 14", { text: last.toFixed(1), color: last > 70 ? "#ef5350" : last < 30 ? "#26a69a" : undefined }],
["Change 24 bars", { text: change.toFixed(2) + "%", align: "right" }],
["Volume", candles.slice(-24).reduce((s, c) => s + c.volume, 0).toFixed(0)],
], { position: "top-right", header: true });

Technical Analysis

The ta class implements the PineScript v5 ta.* functions. Pine works bar by bar, here every function takes whole series (arrays, one value per candle, oldest first) and returns a series of the same length that can be plotted directly. Values are null while the period is not filled yet, and null inputs propagate like Pine's na. Pine functions that read high, low, close or volume implicitly take those arrays as arguments.

Functions returning several series (bb, kc, macd, stoch, dmi, supertrend) return an array that also has named properties: const [middle, upper, lower] = ta.bb(closes, 20, 2) or ta.bb(closes, 20, 2).upper.

Helper Functions

MethodDescription
highest(values, length) / lowest(values, length)Highest / lowest value over length bars
highestbars(values, length) / lowestbars(values, length)Offset (0 or negative) to the highest / lowest bar
max(values) / min(values)All-time high / low up to each bar
range(values, length)highest - lowest
change(values, length = 1)values[i] - values[i - length]
mom(values, length)Momentum
roc(values, length)Rate of change, %
cum(values)Running total
rising(values, length) / falling(values, length)Value is above / below each of the previous length values
variance(values, length, biased = true)Variance
stdev(values, length, biased = true)Standard deviation
dev(values, length)Mean absolute deviation
median(values, length)Median
mode(values, length)Most frequent value (the smallest one on a tie)
percentile_linear_interpolation(values, length, percentage)Percentile, linear interpolation between the two nearest ranks
percentile_nearest_rank(values, length, percentage)Percentile, nearest rank method
percentrank(values, length)% of the previous length values less than or equal to the current one
correlation(series1, series2, length)Correlation coefficient
crossover(series1, series2)Upward crossover (either argument can be a number: ta.crossover(rsi, 70))
crossunder(series1, series2)Downward crossover
cross(series1, series2)Any crossover
barssince(condition)Bars since condition (array of booleans) was last true
valuewhen(condition, values, occurrence = 0)Value of values when condition was true occurrence times ago
pivothigh(values, leftbars, rightbars) / pivotlow(...)Pivot value on the confirming bar, the pivot itself is rightbars bars earlier

Moving Averages

MethodDescription
sma(values, length)Simple moving average
ema(values, length)Exponential moving average
rma(values, length)Wilder's moving average (used by RSI / ATR)
wma(values, length)Weighted moving average
hma(values, length)Hull moving average
alma(values, length, offset = 0.85, sigma = 6, floor = false)Arnaud Legoux moving average
swma(values)Symmetrically weighted moving average over 4 bars
vwma(values, volumes, length)Volume weighted moving average
linreg(values, length, offset = 0)Linear regression curve

Oscillators

MethodDescription
rsi(values, length)Relative Strength Index
cmo(values, length)Chande Momentum Oscillator
cog(values, length)Center of Gravity
mfi(values, volumes, length)Money Flow Index (values is usually hlc3)
tsi(values, shortLength, longLength)True Strength Index, -1..1
wpr(highs, lows, closes, length)Williams %R
stoch(highs, lows, closes, period = 14, smoothK = 3, smoothD = 3)Stochastic [k, d], smoothK = 1 gives the raw value
cci(values, length) / cci(highs, lows, closes, length)Commodity Channel Index (of the typical price for the second form)
macd(values, fast = 12, slow = 26, signalPeriod = 9)MACD [macd, signal, histogram]

Bands and Channels

MethodDescription
bb(values, length = 20, mult = 2) / bollinger(...)Bollinger Bands [middle, upper, lower]
bbw(values, length, mult)Bollinger Bands width (upper - lower) / middle
kc(values, highs, lows, closes, length = 20, mult = 2, useTrueRange = true)Keltner Channels [middle, upper, lower]
kcw(values, highs, lows, closes, length, mult, useTrueRange)Keltner Channels width (upper - lower) / middle

Trend and Volatility

MethodDescription
tr(highs, lows, closes, handleNa = false)True range (handleNa = true gives high - low on the first bar)
atr(highs, lows, closes, period = 14)Average True Range
dmi(highs, lows, closes, diLength = 14, adxSmoothing = 14)Directional Movement [plus, minus, adx]
adx(highs, lows, closes, period = 14)Average Directional Index
sar(highs, lows, closes, start = 0.02, inc = 0.02, max = 0.2)Parabolic SAR
supertrend(highs, lows, closes, factor = 3, atrPeriod = 10)Supertrend [supertrend, direction], direction -1 = up trend, 1 = down trend

Volume

MethodDescription
vwap(values, volumes, anchor?) / vwap(highs, lows, closes, volumes, anchor?)Cumulative VWAP (of the typical price for the second form), restarts on bars where anchor (array of booleans) is true
obv(closes, volumes)On Balance Volume
accdist(highs, lows, closes, volumes)Accumulation / Distribution
iii(highs, lows, closes, volumes)Intraday Intensity Index
pvt(closes, volumes)Price-Volume Trend
nvi(closes, volumes) / pvi(closes, volumes)Negative / Positive Volume Index
wad(highs, lows, closes)Williams Accumulation / Distribution
wvad(opens, highs, lows, closes, volumes)Williams Variable Accumulation / Distribution

Orderbook Helpers

orderbook is source.orderbook() or source.depths()[i].data: {asks: {price: quantity}, bids: {price: quantity}}.

MethodDescription
orderbookSide(orderbook, side)One side ('bids' / 'asks') as [[price, quantity], ...] sorted best price first
bestBid(orderbook) / bestAsk(orderbook)Best bid / ask price (null for an empty book)
sumBids(orderbook, percent, price, inQuote)Cumulative bid quantity between price (default: best bid) and price * (1 - percent / 100), inQuote = true sums price * quantity
sumAsks(orderbook, percent, price, inQuote)Cumulative ask quantity between price (default: best ask) and price * (1 + percent / 100)

Example:

const candles = source.ohlcv();
const closes = candles.map(c => c.close);
const rsiValues = ta.rsi(closes, 14);
plot.lineSeries("RSI", rsiValues, "#00C8FF");

Alerts

Users can create alerts for your indicator from the Alerts tab in the indicator settings on the chart. Alerts are checked on the server side regularly, notifications are sent to Telegram and/or a user defined Webhook URL.

Alert Settings — alertInput

The alertInput class declares alert settings. It works exactly like input, but the fields are shown in the Alerts tab and every user configures their own values for their alert.

Scripts without alertInput declarations can not be used for alerts.

MethodDescription
number(title, defaultValue, min, max, step)Numeric input
range(title, defaultValue, min, max, step)Range input
select(title, defaultValue, values)Dropdown selection
color(title, defaultValue)Color picker
boolean(title, defaultValue)Boolean input

Example:

const alertVolumeThreshold = alertInput.number("Volume threshold", 1000, 0, 10000000, 1);

Firing Alerts — alert.fire

MethodDescription
fire(title, message, date)Fires an alert notification
  • All three parameters are required — a call with a missing title, message or date is skipped (a warning is written to the log).
  • Keep the alert title constant, do not generate unique titles. Each title fires only once per run (max 20 titles per run).
  • date is the candle startTime the signal belongs to: the same title notifies again only when it fires for a newer candle. Signals from candles older than the moment the user created their alert are ignored (anti-spam).

Example:

if (candle.volume > alertVolumeThreshold) {
alert.fire("High volume", `vol ${candle.volume}`, candle.startTime);
}

Shape Alerts — plot.<shape>(...).alert(...)

All geometric shapes (box, square, diamond, triangle, circle, straightLine, horizontalLine, verticalLine, text) return a handle with an .alert() method. The alert fires with the shape's date1 automatically, so the anti-spam logic works without extra code — this is the recommended way for per-candle signals.

Two forms:

// simple: fires every time the shape is plotted for a new date
plot.box(...).alert("High volume", `vol ${candle.volume}`);
// callback: return nothing to skip, [title, message] to fire
plot.box(...).alert((shape) => {
if (candle.volume > alertVolumeThreshold)
alert.fire("High volume", `vol ${candle.volume}`, candle.startTime);
});

The callback receives the shape data (date1, price1, date2, price2, ...).

Series Alerts — plot.<series>(...).alert(...)

Data series (lineSeries, barSeries, candleSeries, areaSeries, bandSeries) also return the .alert() handle. The callback receives the latest data point and the latest candle date, the alert fires with that date.

  • lineSeries / barSeries / areaSeries — the callback receives the last value (a number)
  • candleSeries — the callback receives the last candle {open, close, high, low}
  • bandSeries — the callback receives the last {upper, lower}
plot.lineSeries("RSI", rsiValues, color).alert((rsi, date) => {
if (rsi > alertRsiThreshold) alert.fire("RSI high", `RSI ${rsi.toFixed(1)}`, latestCandle.startTime);
});

A condition that stays true notifies once per new candle. For one-time signals use crossover-style conditions (ta.crossover).

Full Example

const title = "Volume alerts";
const description = "Shows boxes on high-volume candles";
define(title, description, "onchart", true, true);
const color = style.color("Color", "#00C8FF");
const volumeThreshold = input.number("Volume threshold", 1000, 0, 10000000, 1);
const alertVolumeThreshold = alertInput.number("Alert volume threshold", 5000, 0, 10000000, 1);
const alertCloseThreshold = alertInput.number("Alert close threshold", 100000, 0, 10000000, 1);
const candles = source.ohlcv();
const latestCandle = candles.at(-1);
const closes = candles.map(c => c.close);
// series alert: checks the latest close
plot.lineSeries("Closes", closes, color).alert((close, date) => {
if (close > alertCloseThreshold) alert.fire("Close above threshold", `close ${close}`, latestCandle.startTime);
});
for (let i = 0; i < candles.length; i++) {
const candle = candles[i];
if (candle.volume > volumeThreshold) {
const right = i < candles.length - 1 ? candles[i + 1].startTime : candle.startTime + 60000;
// shape alert: checked against the user's alert threshold
plot.box(`Volume ${i}`, candle.startTime, candle.high, right, candle.low, "#00C8FF", 0.3)
.alert(() => {
if (candle.volume > alertVolumeThreshold)
alert.fire("High volume", `vol ${candle.volume}`, candle.startTime);
});
}
}

Examples of indicators

const title = "Your first indicator";
const description = "Shows closes lines on the chart";
const indicatorType = "onchart";
define(title, description, indicatorType, true, true);
const color = input.color("Line Color", "#00C8FF");
const candles = source.ohlcv();
const closes = candles.map(c => c.close);
plot.lineSeries("Closes", closes, color);
define("Example Indicator", "Demonstrates basic plotting", "onchart", true, true);
const color = input.color("Line Color", "#00FF99");
const candles = source.ohlcv();
const closes = candles.map(c => c.close);
const ema20 = ta.ema(closes, 20);
plot.lineSeries("EMA 20", ema20, color);
const title = "Colored Volume Bars";
const description = "Volumes colored based on candle direction.";
const indicatorType = "onchart";
const isPublic = true;
const isPublicCode = true;
define(title, description, indicatorType, isPublic, isPublicCode);
// Get OHLC candles
const candles = source.ohlcv();
const colorUp = style.color("Color 1", "#2BB462"); // Greenish color for rising prices
const colorDown = style.color("Color 2", "#FB4C51"); // Reddish color for falling prices
const volumes = candles.map(candle => candle.volume);
plot.barSeries("Volume", volumes, (value, index) => {
const currentCandle = candles[index];
return currentCandle.close >= currentCandle.open ? colorUp : colorDown;
});
define('Coinbase Premium Index', 'Coinbase Premium Index', 'offchart', true, false);
const color1 = style.color("Color 1", "#2BB462");
const color2 = style.color("Color 2", "#FB4C51");
// Get OHLC candles
const cbCandles = source.ohlcv('coinbase', 'BTC-USD');
const binanceCandles = source.ohlcv('binance', 'BTCUSDT');
// Extract closes
const cbCloses = cbCandles.map(c => c.close);
const binanceCloses = binanceCandles.map(c => c.close);
// --- Coinbase Premium Index calculation ---
const premiumPercent = cbCloses.map((cb, i) => {
const b = binanceCloses[i];
return ((cb - b) / b) * 100;
});
// Plot premium index
plot.barSeries("Premium %", premiumPercent, ((d) => d > 0 ? color1 : color2)) // Percentage difference
const title = "Candles";
const description = "Simple candles draw";
const indicatorType = "offchart";
const isPublic = true;
const isPublicCode = true;
define(title, description, indicatorType, isPublic, isPublicCode);
const colorBuy = style.color("Buy Color", "#2BB462");
const colorSell = style.color("Sell Color", "#7a3d3f");
const candles = source.ohlcv();
const closes = candles.map(c => c.close);
const opens = candles.map(c => c.open);
const highs = candles.map(c => c.high);
const lows = candles.map(c => c.low);
plot.candleSeries(
"Candles",
opens, closes, highs, lows,
(v, i) => {
return opens?.[i] > closes?.[i] ? colorBuy : colorSell
}
);

Order Book Pressure (bid / ask depth imbalance bands around the price, like the "Order Book Pressure" overlays of trading terminals):

const title = "Order Book Pressure";
const description = "Bid / ask depth imbalance bands around the price";
define(title, description, "onchart", true, true);
const minBidRatio = input.number("Min bid ratio %", 20, 0, 100, 1);
const minAskRatio = input.number("Min ask ratio %", 20, 0, 100, 1);
const depthFrom = input.select("Depth from", "0%", ["0%", "1%", "2.5%", "5%"]);
const depthTo = input.select("Depth to", "5%", ["1%", "2.5%", "5%", "10%"]);
const bidColor = style.color("Bid fill color", "#a5d6a7");
const askColor = style.color("Ask fill color", "#f1627e");
const extremeColor = style.color("Extreme color", "#ffff00");
const highlight = style.boolean("Highlight extremes", true);
const showRef = style.boolean("Show reference lines", true);
const alertMinRatio = alertInput.number("Alert min |ratio| %", 40, 0, 100, 1);
const bounds = [1, 2.5, 5, 10]; // depth bands in % from the close
const from = parseFloat(depthFrom);
const to = parseFloat(depthTo);
const candles = source.ohlcv();
const depths = source.depths();
const liveBook = source.orderbook();
// orderbook of every candle: depths()[i] is the snapshot of candles[i] (the biggest limit orders), the latest candle uses the live book
const lastIndex = candles.length - 1;
const books = candles.map((c, i) => {
if (!c) return null;
if (i === lastIndex && ta.bestBid(liveBook)) return liveBook;
return depths[i]?.data || null;
});
const closes = candles.map(c => c ? c.close : null);
// imbalance ratio of every band in % (positive = bids dominate, negative = asks dominate)
const ratios = bounds.map((upper, b) => {
const lower = b === 0 ? 0 : bounds[b - 1];
return candles.map((c, i) => {
const book = books[i];
if (!c || !book) return null;
const bid = ta.sumBids(book, upper, c.close) - ta.sumBids(book, lower, c.close);
const ask = ta.sumAsks(book, upper, c.close) - ta.sumAsks(book, lower, c.close);
const total = bid + ask;
return total > 0 ? ((bid - ask) / total) * 100 : 0;
});
});
// a stronger ratio -> a more opaque band, extreme ratios get the extreme color
const bandOpacity = (ratio) => Math.min(0.95, Math.abs(ratio || 0) / 70 * 0.95);
const bandColor = (ratio) => highlight && Math.abs(ratio || 0) >= 63 ? extremeColor : (ratio >= 0 ? bidColor : askColor);
bounds.forEach((upper, b) => {
const lower = b === 0 ? 0 : bounds[b - 1];
if (from > lower || upper > to) return; // the band is outside of the selected depth range
const bidUpper = closes.map(c => c != null ? c * (1 - lower / 100) : null);
const bidLower = closes.map(c => c != null ? c * (1 - upper / 100) : null);
const askLower = closes.map(c => c != null ? c * (1 + lower / 100) : null);
const askUpper = closes.map(c => c != null ? c * (1 + upper / 100) : null);
if (showRef) {
plot.lineSeries(`Bid ${upper}%`, bidLower, bidColor, 0.4);
plot.lineSeries(`Ask ${upper}%`, askUpper, askColor, 0.4);
}
// bid pressure: the band is filled only on the candles where bids dominate by more than the min ratio
plot.bandSeries(
`Bid pressure ${lower}-${upper}%`,
bidUpper.map((v, i) => ratios[b][i] >= minBidRatio ? v : null),
bidLower,
(upper, lower, i) => bandColor(ratios[b][i]),
(upper, lower, i) => bandOpacity(ratios[b][i])
);
// ask pressure
plot.bandSeries(
`Ask pressure ${lower}-${upper}%`,
askUpper.map((v, i) => ratios[b][i] <= -minAskRatio ? v : null),
askLower,
(upper, lower, i) => bandColor(ratios[b][i]),
(upper, lower, i) => bandOpacity(ratios[b][i])
);
// alert on the latest candle
const latestRatio = ratios[b][lastIndex];
if (latestRatio != null && Math.abs(latestRatio) >= alertMinRatio) {
alert.fire(`Pressure ${lower}-${upper}%`, `ratio ${latestRatio.toFixed(1)}%`, candles[lastIndex].startTime);
}
});