# 0xArchive Terminal scripts The 0xArchive Terminal (a web charting and trading terminal for Hyperliquid and Lighter, with 0xArchive as its data layer) runs indicator scripts written in TypeScript. This file is the complete reference for writing one: the guides, the diagnostic codes, the API (script-api.d.ts), and every example and built-in indicator. It is generated from those files, the same ones the pages at https://docs.0xarchive.io/terminal are built from, so it matches the engine exactly. ## Scripting Scripts run in the [0xArchive Terminal](https://terminal.0xarchive.io), on every market it charts. Beside the chart's bars, a script reads everything 0xArchive records: every trade with its change in open interest, liquidations with the wallet liquidated, where open positions would be liquidated, the full-depth order book, funding, open interest and market breadth. ```ts indicator('Opening flow'); const chart = bars(); const tape = trades(); const opened = tally(); const flow = plot.histogram('New longs less new shorts $'); tape.onTrade((t) => { if (t.oiDelta > 0) opened.add(t.side === 'buy' ? t.notional : -t.notional); }); chart.onBar(() => { flow.set(opened.value, opened.value >= 0 ? color.up : color.down); }); ``` This script splits out the trades that opened positions, using each trade's own change in open interest, and plots new longs less new shorts in USD on every bar. ### What a script is A script is one TypeScript file with no imports. The top level runs once and declares everything: the script's name, its settings, the data it reads, its plots and its state. Handlers then run on every bar of history, oldest first, and on every live update, with the values of that moment. There is no lookahead: history only sees what was known at the time, so a script draws the same values live as it does on history. Scripts run in the browser, in a worker of their own with no network access, and give the same result on every machine. ### Where scripts live The scripts you write are kept in your browser, under the names you give them, and any chart can add them from the Indicators dialog. A script runs on whatever market and interval its chart shows, so one script works on BTC, a HIP-3 stock or a Lighter market alike. Its alerts fire in the browser while the chart is open. The terminal's own studies, from RSI to Supertrend, are scripts too, and so is every example on these pages: open one, copy it into the editor, and change it. ### Start here - [Your first script](https://docs.0xarchive.io/terminal/guides/first-script) writes one step by step and puts it on a chart. - [How a script runs](https://docs.0xarchive.io/terminal/guides/how-scripts-run) gives the rules every script follows. - [Examples](https://docs.0xarchive.io/terminal/examples) shows complete scripts with their chart output, each a click away from a chart. - [Reference](https://docs.0xarchive.io/terminal/reference) lists every function and field. ## Your first script Write a script that draws a 20-period EMA, shades it by trend, and can alert when the price crosses it. Each step adds one idea. ### Open the editor In the [terminal](https://terminal.0xarchive.io), open the chart's menu and choose **New script**, or press `/` for the Indicators dialog and choose **New script**. The editor checks the script as you type and lists its problems underneath; **Add to chart** runs it on the chart, and **Save** keeps it in this browser under its name. ### Name it and read the bars Every script starts with `indicator(name, options)`. `overlay: true` draws on the price pane; without it, the script gets a pane of its own below. ```ts indicator('My EMA', { overlay: true }); const chart = bars(); ``` `bars()` with no options is the chart's own bars. It is a stream: a source of data with a handler that runs on each bar. ### Calculate on every bar Indicators such as `ta.ema` are created once at the top, then updated in a handler. The handler runs once for every bar of history, and again on every update of the bar still forming. ```ts indicator('My EMA', { overlay: true }); const chart = bars(); const ema = ta.ema(20); const line = plot.line('EMA'); chart.onBar(() => { line.set(ema.update(chart.close)); }); ``` Update an indicator on every bar, before any early `return`: it only advances on the bars it is updated on. ### Add a setting `input.*` declares a setting the chart's settings panel shows. Changing it runs the script again from the first bar. ```ts const length = input.int('Length', 20, { min: 1 }); const ema = ta.ema(length); ``` ### Colour by trend A plot's `set` takes an optional colour for that bar. `color.up` and `color.down` are the theme's own, so they follow the reader's theme. ```ts chart.onBar(() => { const value = ema.update(chart.close); line.set(value, chart.close >= value ? color.up : color.down); }); ``` ### Alert on a cross `alert(name)` declares an alert, which the reader arms on the chart. `fire` fires it when the condition is true, at most once per bar, and only on live data. ```ts indicator('My EMA', { overlay: true }); const length = input.int('Length', 20, { min: 1 }); const chart = bars(); const ema = ta.ema(length); const crossUp = ta.crossover(); const line = plot.line('EMA', { width: 2 }); const crossed = alert('Close crossed above the EMA'); chart.onBar(() => { const value = ema.update(chart.close); line.set(value, chart.close >= value ? color.up : color.down); crossed.fire(crossUp.update(chart.close, value), `Close ${fmt.price(chart.close)} crossed the EMA`); }); ``` ## How a script runs Know what may go where in a script and the order its handlers run in. The editor enforces these rules as you type, each with a [diagnostic code](https://docs.0xarchive.io/terminal/reference/diagnostics). ### The shape of a script A script is one TypeScript file with no imports. Its globals are the [API](https://docs.0xarchive.io/terminal/reference). The top level runs once, when the script loads (setup). It may contain only: - `indicator(name, options)`, first; - `const` declarations whose values come from the API's constructors (`input.*`, `bars()`, `trades()` and the other streams, `state.*`, `series`, `tally`, `tallyBy`, `ta.*`, `plot.*`, `alert`, `draw.table`), from literals, or from other constants and helper functions; - `plot.level(...)` calls; - function declarations: pure helpers, or factories called at setup; - handler registrations: `stream.onBar(fn)`, `stream.onClose(fn)`, `trades.onTrade(fn)`, `liquidations.onLiquidation(fn)`. Handlers hold the logic and run per bar or per event. A local variable in a handler starts fresh on every run. Anything that persists is declared [state](https://docs.0xarchive.io/terminal/guides/state). ### What the compiler enforces - Constructors only at the top level; handlers registered only at the top level, unconditionally. - Each state object, series and indicator belongs to the one stream whose handler updates it. - Event handlers (`onTrade`, `onLiquidation`) may change only tallies and live state. - Tallies are filled only by event handlers. - Plots, markers and drawings are set only in the chart's own `bars()` handlers. - No `import` or `export`, `eval`, `with`, `async`, generators, classes, top-level `let` or `var`, top-level collections that change, dynamic indexing of API objects, `Date`, timers, `fetch` or other globals. - `Math` is the engine's own: `exp`, `log`, `pow`, the trigonometric functions and the rest give the same bits in every browser, which can differ in the last bit from a browser console. There is no `Math.random`: use `random(seed)`. ### The order things run in For chart bar `i`, in history: 1. Everything that happened before the bar closed, merged by time: trades and liquidations with a time before its close, and other bar streams' bars that close at or before it (their `onBar` with `confirmed` true, `onClose`, then their commit). At equal times a bar close comes first, and a longer interval before a shorter one. 2. The chart's `onBar`, with `confirmed` true, then its `onClose`. 3. The commit. Tallies empty for the next bar. Live, the forming bar runs again from the last commit on every update, with `confirmed` false. Only the latest update matters, so updates can be skipped without changing the result. > Another stream read from a handler gives its last closed value, "as of" that moment: there is no lookahead. `.forming` on a stream reads its developing bar live, which repaints; on history it is the last closed bar. See [data and timeframes](https://docs.0xarchive.io/terminal/guides/data). An indicator advances only when `update` runs, so update it on every bar. Missing numbers are `na`: arithmetic with `na` is `na`, and comparisons with it are false. See [missing values](https://docs.0xarchive.io/terminal/guides/missing-values). ### When a script runs again The script runs again from setup, over the whole history, when older history is loaded, the market or interval changes, or an input changes. That takes a few tens of milliseconds per 100,000 bars for a typical script. Values near the oldest loaded bar may change, as on TradingView. ## State and indicators Keep a value from one bar to the next. A local variable in a handler starts fresh on every run, so anything that must persist is declared at the top, with a lifetime that says what happens to it while the newest bar is still forming. ### State | Declared with | Kept | |---|---| | A local in a handler | Never: fresh on every run | | `state.bar(x)`, `state.list(n)`, `state.map(n)`, `series(n)`, every `ta.*` instance | Across bars; rolled back before each update of the forming bar, like Pine's `var` | | `state.live(x)`, `state.list(n, 'live')`, `state.map(n, 'live')` | Across bars and across updates of the forming bar, like Pine's `varip` | | `tally()`, `tallyBy(n)` | Within one bar: empty on each new chart bar, and each event is applied once | A cell holds one value, read and written through `.value`: ```ts const highest = state.bar(na); chart.onBar(() => { if (isNa(highest.value) || chart.high > highest.value) highest.value = chart.high; }); ``` Because `state.bar` is rolled back before every update of the forming bar, the update always starts from the last closed bar and nothing is counted twice. Use `state.live` only for something that should count every update, such as how many updates a bar received. ### Lists and maps `state.list(capacity)` and `state.map(capacity)` hold numbers, booleans, strings or `null`, up to 100,000 entries. A map is keyed by strings. Size them for what they will hold: adding past the capacity stops the script with `OX2004`, so `shift` a list's oldest entry before it fills. ```ts const seen = state.map(10_000, 'live'); liq.onLiquidation((l) => { if (l.user !== null) seen.set(l.user, (seen.get(l.user) ?? 0) + l.notional); }); ``` An event handler may change only live state and tallies, which is why this map is `'live'`. ### Series `series(lookback)` is a number with history: `set` the current bar's value, then read `value`, or `ago(n)` for `n` bars back, up to the declared lookback (at most 5,000). ```ts const range = series(20); chart.onBar(() => { range.set(chart.high - chart.low); const widest = Math.max(range.value, range.ago(1), range.ago(2)); }); ``` The bars themselves have history too: `chart.ago(n)` is the bar `n` bars ago. ### Tallies A tally sums what event handlers add during one bar: `add` in `onTrade` or `onLiquidation`, read `value` (and `count`) in the chart's handlers. It empties on each new bar and never rolls back, so each trade counts once even while the bar is forming. ```ts const tape = trades(); const delta = tally(); const deltaBars = plot.histogram('Delta $'); tape.onTrade((t) => delta.add(t.side === 'buy' ? t.notional : -t.notional)); chart.onBar(() => { deltaBars.set(delta.value); }); ``` `tallyBy(capacity)` keeps a sum per key, such as per taker order or per wallet, up to 10,000 keys; read it with `get(key)` or `entries()`. Keys past its capacity are dropped for the rest of the bar. ### Indicators `ta.*` creates an indicator instance at the top; `update` feeds it a value and returns the result. Values match TradingView's built-ins, including their seeds and how they treat `na`. ```ts const rsi = ta.rsi(14); const atr = ta.atr(14); chart.onBar(() => { const r = rsi.update(chart.close); const a = atr.update(chart); }); ``` - **Update on every bar.** An instance advances only when `update` runs, so updating it inside an `if` gives a different result. This is Pine's rule that every call keeps its own history, made visible. - **One instance per use.** Two RSIs of different sources are two instances. - **`value`** is the result of the latest update in this run; **`confirmed`** is the value as of the stream's last closed bar, for reading from another stream's handler. Updated twice in one run, an instance starts the second update again from the bar's start: the last update of the bar wins. ## Data and timeframes Read more than the chart's own bars. Every kind of data 0xArchive records is a stream a script declares at the top, and every stream reads as of the bar being run, so history never sees the future. ### Streams | Stream | What it gives, and how far back | |---|---| | `bars()` | Bars of any market and interval: the chart's own as loaded, another from 300 of its bars before the chart's first, at most 5,000 | | `trades()` | Every trade with its side, size, order and change in open interest; today and the two UTC days before | | `liquidations()` | Every liquidation with its side and the account liquidated: a wallet on Hyperliquid; the last 7 days | | `liquidationLevels()` | Where open positions would be liquidated, 40 levels over 10% either side of the mark, about every 5 minutes; Hyperliquid, the last 7 days | | `orderBook()` | The full-depth book over each bar: mid, spread, size and USD within 0.1% to 10%, imbalance, slippage; Hyperliquid and HIP-3 from 2026-03-10 | | `depth()` | Resting size within bands of the live book the page shows; live only | | `openInterest()` | Open interest over each bar, open, high, low and close; Hyperliquid from 2025-03-01, HIP-3 and Lighter from their starts | | `funding()` | The funding rate over each bar; time bars of 1m or longer | | `derivatives()` | Open interest, funding, mark, oracle and premium from 5-minute samples; the last 14 days | | `breadth()` | The share of a venue's markets above their session VWAP; perps from 24 Aug 2026, HIP-3 from 28 Aug 2026 | Anything a stream does not have reads `na`, `null` or `available: false`, never a guess. Each stream's fields are in the [reference](https://docs.0xarchive.io/terminal/reference/streams). ### Other markets and intervals `bars({ market, interval, venue })` reads another market, another interval, or both. Give it a handler of its own when there is something to calculate on its bars: ```ts indicator('4h EMA', { overlay: true }); const chart = bars(); const h4 = bars({ interval: '4h' }); const ema = ta.ema(20); const line = plot.line('4h EMA', { width: 2 }); h4.onBar(() => { ema.update(h4.close); }); chart.onBar(() => { line.set(ema.confirmed); }); ``` `ema.confirmed` is the EMA as of the last closed 4-hour bar, so the line steps once per 4 hours and draws the same on history as it did live. Intervals are `1m` to `1w`, calendar months `1M` to `12M` (capital M; `1m` is a minute), and bars that close on trades: seconds (`s15`), trade counts (`t500`), volume (`v1000000`), delta, range, renko and reversal. Weeks open on Monday 00:00 UTC. ### As of: no lookahead Inside a handler, every stream other than the handler's own reads its last confirmed value at or before the current moment: just before the bar's close in history, and the newest data live. A 4-hour bar that closes with the last 1-hour bar of its span is visible on that 1-hour bar, as Pine's `request.security` with lookahead off shows it. The one way to read a bar still forming is to ask for it by name, `.forming`: ```ts chart.onBar(() => { const developing = h4.forming.close; }); ``` Live, `forming` is the developing bar, which changes until it closes: it repaints. On history it is the last closed bar, so history still never looks ahead. ### The forming bar The chart's newest bar runs again on every update, each time from the last commit: `confirmed` is false, every `state.bar` value is back where the last closed bar left it, and only the latest run's output is drawn. When the bar closes, `onBar` runs once more with `confirmed` true, then `onClose`. Do something once per bar in `onClose`; do something that should follow the price live in `onBar`. ### Events: trades and liquidations `trades()` and `liquidations()` deliver events to `onTrade` and `onLiquidation`, in execution order, before the `onBar` of the bar they fall in. Sum them into [tallies](https://docs.0xarchive.io/terminal/guides/state#tallies) and read the tallies in the chart's handlers; see [order flow](https://docs.0xarchive.io/terminal/guides/order-flow) and [liquidations](https://docs.0xarchive.io/terminal/guides/liquidations). `available` says whether the stream covers the current bar completely: older bars than its history have none. ### Sample streams `orderBook()`, `openInterest()`, `funding()`, `derivatives()`, `breadth()`, `depth()` and `liquidationLevels()` have no handlers. Read them in any handler; each gives its value as of that moment, or `na` with `available: false` when there is none. ```ts const oi = openInterest(); chart.onBar(() => { if (!oi.available) return; oiChange.set(oi.change({ usd: true })); }); ``` ## Order flow Build order flow from the tape: every taker fill on the market, with what 0xArchive records about it. `trades()` reaches back to the start of the UTC day before yesterday, from 0xArchive's history, and continues live from the chart's tape. ### A trade | Field | What it is | |---|---| | `time`, `price`, `size`, `notional` | When, at what price, how much in the market's coin, and in USD | | `side` | The taker's side, `'buy'` or `'sell'` | | `oiDelta` | The trade's change in open interest: positive where it opened positions, negative where it closed them | | `sameOrder` | Whether the same taker order filled the trade before it | | `takerOrderId`, `tradeId` | Ids, on live trades; `null` in history | | `liquidation` | Whether the taker was being liquidated | | `taker`, `maker` | Wallets, where the source carries them; `null` in the terminal today | ### Sum the tape per bar Add each trade to a [tally](https://docs.0xarchive.io/terminal/guides/state#tallies) in `onTrade`, and read it in the chart's `onBar`. This is volume delta, in USD: ```ts indicator('Volume delta'); const chart = bars(); const tape = trades(); const delta = tally(); const deltaBars = plot.histogram('Delta $'); tape.onTrade((t) => delta.add(t.side === 'buy' ? t.notional : -t.notional)); chart.onBar(() => { if (!tape.available) return; deltaBars.set(delta.value, delta.value >= 0 ? color.up : color.down); }); ``` `minSize` and `minNotional` filter the stream, so `trades({ minNotional: 100_000 })` delivers only trades of $100k or more. ### Opening and closing trades 0xArchive records each trade's change in open interest (`oiDelta`), in the market's coin. With the taker's side it says what the trade did: | Taker trade | What happened | |---|---| | Buy, `oiDelta` above 0 | A new long | | Buy, `oiDelta` below 0 | A short bought back | | Sell, `oiDelta` above 0 | A new short | | Sell, `oiDelta` below 0 | A long sold out | An `oiDelta` of 0 means one side opened while the other closed. Hyperliquid and HIP-3 count both sides of open interest, Lighter longs only, as each venue's own open interest does. [Opening and closing flow](https://docs.0xarchive.io/terminal/examples/opening-closing-flow) and [Squeeze or new positions](https://docs.0xarchive.io/terminal/examples/squeeze-or-new-positions) are built on it. ### Taker orders One market order fills against many resting orders, and each fill is a trade. `sameOrder` is true when the same taker order filled the trade before it, so a new order starts wherever it is false. That joins fills into orders across the whole history, where `takerOrderId` is `null`: ```ts const order = state.live(0); const orders = tallyBy(512); tape.onTrade((t) => { if (!t.sameOrder) order.value += 1; orders.add(String(order.value), t.side === 'buy' ? t.notional : -t.notional); }); ``` [Large taker orders](https://docs.0xarchive.io/terminal/examples/large-takers) marks the bars where one came to $1M or more. ### Order flow by bar, without events Bars that close on trades (`t500`, `v1000000` and the rest) carry `buyVolume` and `sellVolume` on each bar. Time bars do not split volume, so sum `trades()` for them. ## Liquidations Follow liquidations two ways: the ones that happened, with the account liquidated, and the ones waiting, as the price levels where open positions would be liquidated. ### Liquidations that happened `liquidations()` delivers each liquidation to `onLiquidation`: `time`, `price`, `size`, `notional`, `side` (the side of the position liquidated, `'long'` or `'short'`) and `user`, the account. On Hyperliquid `user` is the wallet, lower-case; on Lighter it is an account index. Liquidations reach back 7 days. ```ts indicator('Liquidations', { precision: 0 }); const chart = bars(); const liq = liquidations({ minNotional: 10_000 }); const longs = tally(); const shorts = tally(); const longBars = plot.histogram('Longs liquidated $'); const shortBars = plot.histogram('Shorts liquidated $'); liq.onLiquidation((l) => (l.side === 'long' ? longs : shorts).add(l.notional)); chart.onBar(() => { if (!liq.available) return; longBars.set(-longs.value, color.down); shortBars.set(shorts.value, color.up); }); ``` ### Follow wallets `input.addresses` gives the settings panel a wallet list, which keeps only valid addresses, lower-case. Skip the others in the handler: ```ts const wallets = input.addresses('Only these wallets', []); liq.onLiquidation((l) => { if (wallets.length > 0 && !(l.user !== null && wallets.includes(l.user.toLowerCase()))) return; (l.side === 'long' ? longs : shorts).add(l.notional); }); ``` Lighter names an account index rather than a wallet, which no address matches. One position is often liquidated in several fills a moment apart. To count liquidations rather than fills, treat a wallet's fills within a minute as one, as [Most-liquidated wallets](https://docs.0xarchive.io/terminal/examples/liquidated-wallets) does. ### Where liquidations would happen next `liquidationLevels()` is 0xArchive's estimate of where open positions would be liquidated: 40 levels over 10% either side of the mark, each with the USD of longs and of shorts liquidated there, about every 5 minutes. Read it in any handler; `levels()` is the snapshot as of that bar, sorted by price. ```ts const map = liquidationLevels(); chart.onBar(() => { if (!map.available) return; let shortsAbove = 0; for (const level of map.levels()) if (level.price > chart.close) shortsAbove += level.shortUsd; }); ``` Levels are Hyperliquid's, from 27 July 2026, kept for 7 days, and never interpolated between snapshots: a bar between two snapshots reads the earlier one. Levels far from the price stretch the price scale and squash the candles; plot them with `autoscale: false` so they are drawn where they fall without moving the scale. [Liquidation levels ahead](https://docs.0xarchive.io/terminal/examples/liquidation-levels-ahead) and [Liquidation clusters](https://docs.0xarchive.io/terminal/examples/liquidation-clusters) draw them. ## Outputs Show what a script found. Plots are declared at the top and set in the chart's handlers; drawings and tables are objects a handler creates and changes. ### Plots | Plot | What it draws | |---|---| | `plot.line` | A line, solid, dashed or dotted | | `plot.histogram` | Bars from a base, 0 by default | | `plot.area` | A line with its area filled | | `plot.points` | Dots or crosses, with no line between them | | `plot.candles` | Candles of the script's own | | `plot.fill` | A shade between two plots or levels | | `plot.level` | A horizontal line across the pane, such as 70 on an RSI | | `plot.background` | A shade behind the whole height of the pane | | `plot.barColor` | The chart's own candles, recoloured | `set(value, color)` sets the current bar; a plot not set on a bar is `na` there, and a line breaks across it unless it says `join: true`. A colour left out, or `null`, follows the theme's study palette. Up to 64 plots. ```ts const basis = plot.line('Basis', { width: 2 }); const upper = plot.line('Upper', { color: color.withAlpha(color.study(3), 0.6) }); const lower = plot.line('Lower', { color: color.withAlpha(color.study(3), 0.6) }); const band = plot.fill('Band', upper, lower, { color: color.withAlpha(color.study(3), 0.08) }); ``` ### Panes and scales An overlay script (`indicator(name, { overlay: true })`) draws on the price pane; any other draws in a pane of its own. A plot can choose with `pane`: `'price'`, `'own'`, or a name, which gives the script another pane. - `autoscale: false` draws a plot at its values without stretching the scale, for levels far from the price. - `range: { min: 0, max: 100 }` fixes the scale, as an RSI's. - `band: { top: 0.84, bottom: 0 }` draws the plot on a hidden scale in a slice of the pane, such as volume at its foot. - `offset` shifts a plot by whole bars, -500 to 500: positive draws into the space past the newest bar, like Ichimoku's leading spans. ### Shapes and markers `plot.shape` draws a shape, a label or a character on the bars where `set(condition)` is true, at a fixed `location`. `plot.markers` adds any number of markers to a bar, each with its own position, shape, text and colour: ```ts const marks = plot.markers('Big orders'); chart.onClose(() => { if (buyUsd.value > 1_000_000) marks.add({ position: 'below', shape: 'circle', text: fmt.usd(buyUsd.value), color: color.up }); }); ``` ### Drawings `draw.line`, `draw.box`, `draw.label`, `draw.polyline` and `draw.linefill` place objects at times or bar numbers and prices, up to 500 bars into the future. They are made in the chart's handlers, and a drawing made while the forming bar updates is rolled back with it. Each kind is capped, 50 by default and at most 500 (`maxLines` and the others in `indicator`'s options), with the oldest removed first. A common pattern draws only on the newest bar, with the cap set to what one bar draws, so each bar's drawings replace the last's: ```ts indicator('Range lines', { overlay: true, maxLines: 2 }); const chart = bars(); const high = ta.highest(50); const low = ta.lowest(50); chart.onBar(() => { const top = high.update(chart.high); const bottom = low.update(chart.low); if (!chart.isLast) return; draw.line({ from: { bar: chart.index - 50, price: top }, to: { bar: chart.index, price: top }, extend: 'right' }); draw.line({ from: { bar: chart.index - 50, price: bottom }, to: { bar: chart.index, price: bottom }, extend: 'right' }); }); ``` ### Tables `draw.table({ position, columns, rows })` puts a table in one of nine positions of the script's pane, clear of the legend and the price axis; it may be made at the top. `cell(column, row, text, options)` writes a cell, with colours, size, alignment and a tooltip; `merge` spans cells. Write the cells on the newest bar only (`chart.isLast`). Text is shown as text: no markup, links or images, at most 500 characters. Up to 9 tables. ### Text and colour - `fmt.price(x)` writes a price to the market's precision; `fmt.usd(x)` writes `$1.2M`; `fmt.pct(0.0123)` writes `1.23%`; `fmt.num` and `fmt.compact` the rest. - `color.up`, `color.down`, `color.neutral` and `color.panel` are the theme's, and `color.study(i)` its study palette, so a script follows the reader's theme. `color.withAlpha(c, a)`, `color.mix(a, b, t)` and `color.rgb` make others. ### Alerts `alert(name, { message })` declares an alert at the top, which the reader arms on the chart; `fire(condition, message)` fires it from a handler. An alert fires at most once per bar, only on live data, and by default only when a bar closes; `onForming: true` also fires on the forming bar, which repaints. Alerts fire in the browser while the chart is open. ## Inputs Let the reader tune a script without editing it. Each `input.*` at the top declares a setting in the chart's settings panel and returns its value. Changing one runs the script again from its first bar. | Input | The setting | |---|---| | `input.int`, `input.float` | A number, with `min`, `max` and `step` | | `input.bool` | A switch | | `input.string` | Text | | `input.choice` | One of a list of strings | | `input.source` | A bar value such as `close` or `hl2`, read with `chart.source(name)` | | `input.color` | A colour; `null` follows the theme | | `input.time` | A moment picked by clicking the chart, `null` until picked | | `input.interval` | An interval; `'chart'` is the chart's own | | `input.market` | A market such as `ETH` or `xyz:GOLD`; `null` is the chart's | | `input.addresses` | A list of wallet addresses, lower-case | ```ts const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const against = input.market('Against', 'ETH'); const corner = input.choice('Position', 'top-right', ['top-left', 'top-right'], { legend: false }); ``` - `group` puts inputs under a heading, `inline: true` on the row of the one before, and `tooltip` explains one. - The legend names a script with its numbers, sources, choices and intervals, as in "RSI 14 close". `legend: false` keeps an input out of it, for a setting that says where something draws rather than what it computes. ### What changing an input does Every input is read once, at setup, so its value is a plain constant everywhere in the script: use it to size an indicator (`ta.ema(length)`), to pick a market for a stream, or in any handler. Changing it in the settings panel runs the script again from its first bar with the new value, so the whole history is drawn as if it had always been set that way. - An `input.time` starts `null`, and the settings panel offers **Pick on chart**: the reader clicks a bar, and the script reads its time. Draw nothing until one is picked, or start from the first bar. - An `input.interval` set to `'chart'` follows the chart: the script reads the chart's own code, such as `'15m'`, and `bars({ interval })` with it is the chart's own stream. - An `input.market` left `null` follows the chart's market, so `bars(against === null ? {} : { market: against })` reads the chart itself. - `input.addresses` takes pasted addresses; the script receives only the valid ones, lower-case. ## Missing values Handle data that is not there. A missing number is `na`, a float NaN, as in Pine; missing strings, colours and objects are `null`. - Arithmetic with `na` gives `na`, and every comparison with it is false, including `na === na`. - `isNa(x)` tests for it; `nz(x, replacement)` replaces it, with 0 by default. - A plot set to `na` draws nothing on that bar: a line breaks there unless it says `join: true`. - An indicator updated with `na` follows TradingView's rule for that indicator. Because comparisons with `na` are false, test for the condition you want rather than its opposite: ```ts // Draws nothing until the EMA has a value, then colours each bar. const value = ema.update(chart.close); if (!isNa(value)) line.set(value, chart.close > value ? color.up : color.down); ``` ### Missing strings, colours and objects Anything that is not a number is `null` when it is missing: a market input left on the chart's own, a colour input that follows the theme, the wallet on a Lighter liquidation. Check before using one, as for any nullable value in TypeScript: ```ts liq.onLiquidation((l) => { if (l.user === null) return; seen.set(l.user, (seen.get(l.user) ?? 0) + l.notional); }); ``` A colour of `null` passed to a plot's `set` means the plot's own colour, not none; use `color.none` to draw nothing on that bar. ### In state and history A series bar that was never set reads `na`, and so does `ago(n)` before the first bar. A `state.bar(na)` cell is a common way to mean "not known yet": the first bar that knows the value writes it, and every bar after reads it. ```ts const firstClose = state.bar(na); chart.onBar(() => { if (isNa(firstClose.value)) firstClose.value = chart.close; change.set(chart.close / firstClose.value - 1); }); ``` ### Where data starts Each stream has a start: trades the start of the UTC day before yesterday, liquidations 7 days back, the order book 2026-03-10. Before it, a stream's `available` is false and its values are `na`. Check `available` before using a stream, so older bars stay empty instead of showing zeros: ```ts chart.onBar(() => { if (!tape.available) return; deltaBars.set(delta.value); }); ``` ## Pine Script Bring an indicator from TradingView. Paste a Pine Script® v5 or v6 `indicator()` script into the script editor and it is translated into a TypeScript script when it loads, with any problems reported against your Pine line numbers. **Show as TypeScript** in the editor shows what it became, to read or keep editing. Pine Script is a registered trademark of TradingView, Inc. ### How Pine maps to a script | Pine | Here | |---|---| | A plain variable | A local inside the chart's `onBar` | | `var x = ...` | `const x = state.bar(...)` at the top | | `varip x = ...` | `const x = state.live(...)` | | `x[n]` | A `series` read with `.ago(n)` | | `ta.ema(src, 20)` | An instance per call, `ta.ema(20)`, updated where the call was | | `request.security(syminfo.tickerid, tf, expr)` | A second `bars({ interval })` stream, read closed only | | `request.security` on another ticker | A stream on that market, for Binance, Bybit and OKX crypto tickers | | `request.footprint`, `footprint.*` | Built from `trades()`, where the trade history reaches | | `plot`, `plotshape`, `fill`, `bgcolor`, `barcolor`, `hline` | `plot.line` and the other plots | | `line.*`, `label.*`, `box.*`, `table.*` | `draw.*` | | `alertcondition` | `alert` | `request.security` reads what the other timeframe had closed, which is Pine's behaviour with lookahead off; the public API has no way to look ahead. ### What it refuses Each with a message saying what to do: - `strategy()` scripts: strategies come later. - Pine v4 and older: TradingView's Pine Editor converts a script to a newer version. - `import` of libraries. - Data only TradingView has: `request.financial`, `request.economic`, `request.earnings`, `request.dividends`, `request.splits`, `request.seed` and `request.quandl`, and macro tickers such as `TVC:DXY`. Diagnostics from the translation start with `PN`: `PN1xxx` parsing, `PN2xxx` types and meaning, `PN3xxx` not supported. ## Limits and determinism Know the limits a script runs within. They are the same on every device and every plan. | Limit | Value | |---|---| | Steps (loop iterations and function calls) | 10 million for setup, per batch of 512 bars, and per live update | | Plots | 64 | | Series lookback | 5,000 bars | | `tallyBy` keys | 10,000 | | State lists and maps | 100,000 entries | | Drawings | 50 per kind by default, 500 at most | | Tables | 9 | | Labels and table cells | 500 characters | | A single run | 5 seconds | Past the step budget a script stops with `OX2200`, naming the line of the loop or call, and keeps what it had drawn. Counting steps rather than time makes the limit behave the same on a phone and a desktop. A run that takes more than 5 seconds is stopped, and the same script with the same settings is not loaded again on the page until you change it. ### Keeping a script fast - Work per bar, not per bar times history: keep running totals in [state](https://docs.0xarchive.io/terminal/guides/state) rather than looping back over old bars. - Draw only on the newest bar (`chart.isLast`) when only the newest bar's drawing matters. - Sum events into tallies in their handlers; do the per-bar work once in `onBar` or `onClose`. ### The same result everywhere A script gives the same output for the same data in every browser: - no clock, timers or `Math.random`: `random(seed)` gives the same numbers for the same seed; - `Math` is the engine's own, a port of fdlibm, so `exp`, `log`, `pow` and the trigonometric functions give the same bits in Chrome, Safari and Firefox; - `ta.*` follow TradingView's formulas, seeds and running sums. Scripts run in a worker of their own, served with a policy that allows no network access, and output is checked against the script's declaration before it is drawn. ## Tools for agents Have an AI write scripts for you. The whole reference comes as one text file, and every problem the editor reports has a stable code a model can act on. ### The reference for an AI [terminal/llms.txt](https://docs.0xarchive.io/terminal/llms.txt) is the whole scripting reference in one file: these guides, every diagnostic code, the API's TypeScript declarations, every example and every built-in indicator. It is built from the same sources as these pages and the engine, so it matches what the terminal runs. Give it to a model with your request, then paste the script into the editor. ### What to ask for A model writes a better script when the request says what it should read and draw. Name: - the data: the chart's bars, another interval or market, the tape, liquidations, liquidation levels, the order book, open interest, funding or breadth; - what to plot and where: on the price pane, in a pane of its own, as a line, bars, markers or a table; - the settings the reader should be able to change; - what should happen on the bar still forming: follow the price live, or wait for the bar to close. For example: "Using the 0xArchive scripting reference, write a script that plots, in its own pane, the USD of long liquidations less short liquidations on each bar, with a setting for the smallest liquidation to count, and marks bars where more than $5M of longs were liquidated." The rules a model most often breaks are in [how a script runs](https://docs.0xarchive.io/terminal/guides/how-scripts-run): constructors only at the top, persistent values as declared state, and indicators updated on every bar. ### Fixing what it wrote The editor checks a script as you type and lists each problem with its line and a code, such as `OX1003` for a top-level `let`. Give the model the list: each code's entry in [diagnostics](https://docs.0xarchive.io/terminal/reference/diagnostics) says what is wrong and what to do. Codes never change meaning, so a fix-up loop can key on them. `OX9xxx` codes are the engine's own failures, never the script's. ## Diagnostic codes | Code | Severity | What | What to do | |---|---|---|---| | OX1000 | error | The source does not parse. | Fix the syntax at the line shown. | | OX1001 | error | The script must start with indicator(...). | Make `indicator(name, options)` the first statement. | | OX1002 | error | This statement cannot run at the top level. | The top level may hold only indicator(), const declarations, plot.level(), function declarations and handler registrations. Move logic into a handler such as `chart.onBar(() => { ... })`. | | OX1003 | error | Top-level let and var are not allowed. | Persistent values are declared state: use `const x = state.bar(0)` and write `x.value` in a handler. | | OX1004 | error | A top-level collection is changed after setup. | Top-level arrays, objects, Maps and Sets are constants. Use `state.list(n)` or `state.map(n)` for data that changes. | | OX1006 | error | indicator(...) may be called only once, at the top level. | Keep one `indicator(...)` call, as the first statement. | | OX1010 | error | Scripts have no imports or exports. | A script is one file. Everything it can use is a global of the script API. | | OX1011 | error | eval and the Function constructor are not available. | Write the code directly. | | OX1012 | error | The with statement is not allowed. | Name the object explicitly. | | OX1013 | error | async and await are not available. | Handlers run synchronously per bar or event; data arrives through streams. | | OX1014 | error | Generators are not available. | Use a loop or an array. | | OX1015 | error | Classes are not available. | Use plain objects and functions; persistent values are declared state. | | OX1016 | error | `this` is not available at the top level. | Name the value you mean. | | OX1017 | error | finally blocks are not allowed. | A finally block can swallow the step limit. Use try/catch without finally. | | OX1018 | error | debugger statements are not allowed. | Use `log.info(...)` to inspect values. | | OX1019 | error | This TypeScript construct is not supported. | Scripts use erasable TypeScript only: no enum, namespace, decorators or parameter properties. Use a union of string literals instead of an enum. | | OX1020 | error | This global is not available in scripts. | Scripts see only the script API and plain JavaScript values (Math, Number, String, Array, Object, JSON, Map, Set). | | OX1021 | error | Math.random is not available. | Use `random(seed)`, which gives the same numbers everywhere. | | OX1022 | error | constructor, prototype and __proto__ are not accessible. | Work with the values directly. | | OX1023 | error | Globals and API objects cannot be assigned to. | Declare your own value, or write a state cell’s `.value`. | | OX1024 | error | **= works on a plain variable only. | Write `a.b = a.b ** c`. | | OX1025 | error | Names starting with __ are reserved for the engine. | Rename the variable. | | OX1101 | error | This constructor can be called only at the top level (setup). | Create inputs, streams, state, indicators, plots and alerts once at the top level; update them in handlers. | | OX1103 | error | Handlers must be registered at the top level, unconditionally. | Write `stream.onBar(...)` as its own top-level statement. | | OX1104 | error | API objects cannot be indexed dynamically. | Name the member: `chart.close`, or read a bar value by name with `chart.source(name)`. | | OX1105 | error | This state is updated from two streams’ handlers. | Each state object, series and indicator belongs to one stream. Give each stream its own, and read the other stream’s value instead of writing it. | | OX1106 | error | Event handlers may change only tallies and live state. | Events are never replayed, so they cannot change state that rolls back. Add to a `tally()` or `tallyBy(n)`, or use `state.live(...)`, and use the result in the chart’s onBar. | | OX1107 | error | Tallies are filled by event handlers. | Call `tally.add(...)` in `trades.onTrade` or `liquidations.onLiquidation`. In a bar handler, use a local or `state.bar(...)`. | | OX1108 | error | Plots, markers and drawings are set from the chart’s handlers. | Set them in the onBar or onClose of the chart’s own `bars()` stream, reading the other stream there. | | OX1109 | error | This stream has no such handler. | Bar streams have onBar and onClose, trades() has onTrade, liquidations() has onLiquidation. Sample streams have no handlers: read them in a bar handler. | | OX1110 | error | A handler must be a function. | Pass an arrow function, or the name of a top-level function. | | OX1901 | warning | This indicator is updated inside a condition. | An indicator advances only when update runs, so it skips the bars where the condition is false (Pine’s call-site rule). Update it on every bar and use the value conditionally, unless that is what you mean. | | OX2001 | error | A constructor was called inside a handler. | Create it once at the top level and update it in the handler. | | OX2002 | error | This can be called only inside a handler. | Indicators update and handlers register at the times docs/scripting.md §2 sets out. | | OX2003 | error | This state cell holds numbers. | Its initial value was a number. Declare `state.bar(null)` or similar for a cell that can hold other values. | | OX2004 | error | A state list, map or tally is full. | Raise its capacity (up to the limit) or remove old entries. | | OX2005 | error | A size is over the limit. | Series keep at most 5,000 bars, tallyBy 10,000 keys, lists and maps 100,000 entries, drawings 500 per kind. | | OX2006 | error | A series was read further back than its lookback. | Declare a larger lookback: `series(n)`. | | OX2007 | error | More than 64 plots. | Combine plots, or split the script. | | OX2008 | error | An input is declared wrongly. | Check the default value and the options. | | OX2009 | error | Plots, markers and drawings can be set only from the chart’s handlers. | Set them in the chart stream’s onBar or onClose. | | OX2010 | error | An argument is not valid. | Check the value named in the message. | | OX2011 | error | Unknown time zone. | Use an IANA name such as `America/New_York`, or `UTC`. | | OX2012 | error | More than 9 tables. | Reuse a table: clear it and write new cells. | | OX2013 | error | A list index is out of range, or the list is empty. | Check `list.length` first. | | OX2100 | error | The script threw an error. | Read the message and the line. | | OX2105 | error | This state is updated from two streams’ handlers. | Each state object, series and indicator belongs to the stream whose handler first updated it. | | OX2106 | error | An event handler changed state that rolls back. | Event handlers may change only tallies and live state. | | OX2107 | error | A tally was added to outside an event handler. | Fill tallies in onTrade or onLiquidation. | | OX2200 | error | The step limit ran out. | A script gets 10 million steps (loop iterations and function calls) for setup, per batch of 512 bars and per live update. Look for a loop that does not end, or work that can move out of the loop. | | OX9001 | error | The engine failed on this script, which is our bug, not yours. | Please report the script. Simplifying what the message names (a very deeply nested expression, say) may get past it meanwhile. | | OX9002 | error | The engine could not read the data this script reads, which is our bug, not yours. | Please report it. Reloading the page starts the data over. | ## The API: script-api.d.ts ```ts /** * The oxterminal script API: every global a script can use. * * This file is the contract between script authors (people, AI agents and the Pine front end) * and the engine. The rules it relies on are in docs/scripting.md. It is ambient on purpose: * scripts have no imports. Only script sources and the engine's own checks include it; app code * must never import it, or these names would leak into the app's global scope. * * Writing style for the doc comments: they are the editor's autocomplete and the AI's reference, * so each says what the thing does, when it may be called, and what it returns when data is missing. */ // --------------------------------------------------------------------------------------------- // Basic types // --------------------------------------------------------------------------------------------- /** Milliseconds since the Unix epoch, UTC. */ type Time = number; /** * A colour: `#rrggbb`, `#rrggbbaa`, or a theme token from `color.*` (`theme:up`, `theme:down`, * `theme:neutral`, `theme:panel`, `theme:none`, `theme:study:<0-15>`, each with an optional alpha suffix such as * `theme:up@0.3`). `null` means "the theme's colour for this output", which is what a plot uses * when no colour is given. */ type Color = string; /** A price-derived value of the bar a source input can pick. */ type SourceName = 'open' | 'high' | 'low' | 'close' | 'hl2' | 'hlc3' | 'ohlc4' | 'hlcc4' | 'volume'; /** Which venue a market trades on. Defaults to the chart's venue. */ type Venue = 'hl' | 'hip3' | 'lighter'; /** * An interval code. Time: `1m` `3m` `5m` `15m` `30m` `1h` `2h` `4h` `8h` `12h` `1d` `1w`, and calendar * months `1M` `2M` `3M` `4M` `6M` `12M` (capital M; `1m` is a minute). Weeks open on Monday * 00:00 UTC and months on the 1st, from January. Bars that close on trades (see src/browser/trade-bars.mjs): `s15` seconds, `t500` trades, * `v1000000` volume, `d250000` delta, `r10` range, `k10` renko, `x10-0` reversal. */ type IntervalCode = string; /** Where a value is drawn: the price pane, the script's own pane (`'own'`), or a named pane of this script. */ type PaneName = 'price' | 'own' | (string & {}); type LineStyle = 'solid' | 'dashed' | 'dotted'; type LineWidth = 1 | 2 | 3 | 4; /** * A shape to mark a bar with. `labelUp` and `labelDown` are labels with a pointer: `labelUp` * sits below its point and points up at it, `labelDown` sits above and points down; their * `text` is written inside. */ type ShapeName = 'circle' | 'square' | 'diamond' | 'cross' | 'xcross' | 'triangleUp' | 'triangleDown' | 'arrowUp' | 'arrowDown' | 'flag' | 'labelUp' | 'labelDown'; type ShapeLocation = 'above' | 'below' | 'top' | 'bottom' | 'absolute'; type Size = 'tiny' | 'small' | 'normal' | 'large' | 'huge'; /** * `na` is the missing number, a float NaN: arithmetic with it gives `na`, and every comparison * with it is false, as in Pine. Missing strings, colours and objects are `null`. */ declare const na: number; /** `x`, or `replacement` (default 0) when `x` is `na`. */ declare function nz(x: number, replacement?: number): number; /** Whether `x` is `na` (for numbers) or `null`. */ declare function isNa(x: unknown): boolean; // --------------------------------------------------------------------------------------------- // The declaration. Call once, first. // --------------------------------------------------------------------------------------------- interface IndicatorOptions { /** Draw on the price pane (true) or in the script's own pane below it (false, the default). */ overlay?: boolean; /** A short name for the legend. */ shortName?: string; /** Decimal places for values; defaults to the market's price precision on the price pane. */ precision?: number; /** `'percent'`: values in the script's own pane (axis, tags, legend) print with a % sign. Set the values in percent. */ format?: 'percent'; /** Caps on drawing objects, oldest removed first. Default 50 each, at most 500. */ maxLines?: number; maxLabels?: number; maxBoxes?: number; maxPolylines?: number; maxLinefills?: number; } /** Names the script and says where it draws. Must be the first statement. */ declare function indicator(name: string, options?: IndicatorOptions): void; // --------------------------------------------------------------------------------------------- // Inputs: settings the user changes in the settings panel. Declared at the top level only. // Changing an input re-runs the script from its first bar. // --------------------------------------------------------------------------------------------- interface InputOptions { group?: string; tooltip?: string; /** Show the input on the same row as the previous one. */ inline?: boolean; /** * `false` keeps the input's value out of the script's name in the legend, which otherwise * carries its numbers, sources, choices and intervals ("RSI 14 close"). For a setting that * says where something draws rather than what it computes, such as a table's corner. */ legend?: boolean; } interface NumberInputOptions extends InputOptions { min?: number; max?: number; step?: number; } declare const input: { int(label: string, defaultValue: number, options?: NumberInputOptions): number; float(label: string, defaultValue: number, options?: NumberInputOptions): number; bool(label: string, defaultValue: boolean, options?: InputOptions): boolean; string(label: string, defaultValue: string, options?: InputOptions): string; /** One of a fixed list of strings. */ choice(label: string, defaultValue: T, choices: readonly T[], options?: InputOptions): T; /** Which bar value to read, such as `close` or `hl2`. Read it with `bars().source(name)`. */ source(label: string, defaultValue: SourceName, options?: InputOptions): SourceName; /** A colour; `null` follows the theme. */ color(label: string, defaultValue: Color | null, options?: InputOptions): Color | null; /** A moment the user picks by clicking the chart, such as an anchored VWAP's start. `null` until picked. */ time(label: string, defaultValue: Time | null, options?: InputOptions): Time | null; /** * An interval code. `'chart'` stands for the chart's own interval, as a default and as the * user's choice: the script then reads the chart's code (`'15m'` on a 15-minute chart), and a * `bars({ interval })` stream with it is the chart's own stream. */ interval(label: string, defaultValue: IntervalCode | 'chart', options?: InputOptions): IntervalCode; /** A market symbol such as `BTC` or `xyz:GOLD`; `null` means the chart's market. */ market(label: string, defaultValue: string | null, options?: InputOptions): string | null; /** A list of wallet addresses, lower-case hex. */ addresses(label: string, defaultValue: readonly string[], options?: InputOptions): readonly string[]; }; // --------------------------------------------------------------------------------------------- // Streams: the data a script reads. Declared at the top level only. // // Inside a handler of stream S, S's fields describe the bar or event being run. Every other // stream reads "as of" that moment: its last confirmed value at or before the current bar's // time. That is the no-lookahead rule, for every kind of data. // --------------------------------------------------------------------------------------------- interface MarketInfo { readonly symbol: string; readonly venue: Venue; /** The smallest price step at the current price. Hyperliquid's step changes with price. */ readonly tickSize: number; readonly sizeDecimals: number; } interface IntervalInfo { readonly code: IntervalCode; /** Length in milliseconds for time bars (for months, the average month); `na` for bars that close on trades. */ readonly ms: number; readonly isTime: boolean; } /** One bar. Values the source does not provide are `na`. */ interface BarView { readonly time: Time; /** When the bar ends (exclusive). For time bars, `time + interval.ms`; for months, the next month's open. */ readonly closeTime: Time; readonly open: number; readonly high: number; readonly low: number; readonly close: number; /** Base volume. */ readonly volume: number; /** Quote (USD) volume. */ readonly quoteVolume: number; readonly tradeCount: number; /** Taker buy and sell base volume, when the host has them. */ readonly buyVolume: number; readonly sellVolume: number; /** A bar value by name, such as `source('hl2')`. */ source(name: SourceName): number; } interface BarsOptions { /** A market other than the chart's, such as `ETH`. */ market?: string; venue?: Venue; /** An interval other than the chart's, such as `4h`. */ interval?: IntervalCode; } interface Bars extends BarView { readonly market: MarketInfo; readonly interval: IntervalInfo; /** Bar number on this stream, from 0 at the first loaded bar. */ readonly index: number; /** True when this run is for a closed bar; false for an update of the forming bar. */ readonly confirmed: boolean; /** True on the first run of a bar. */ readonly isNew: boolean; /** True on the newest bar the chart has. */ readonly isLast: boolean; /** * The index of this stream's newest bar, known on every bar of history (Pine's * `last_bar_index`). It grows as new bars arrive; bars already run keep the value they saw. */ readonly lastIndex: number; /** The open time of this stream's newest bar, known on every bar (Pine's `last_bar_time`). */ readonly lastTime: Time; /** True once the run has reached live data. */ readonly isRealtime: boolean; /** The bar `n` bars ago on this stream; `ago(0)` is the current one. Fields are `na` before the first bar. */ ago(n: number): BarView; /** * The developing bar of this stream, read from another stream's handler. It changes until the * bar closes (it repaints), which is why it has to be asked for by name. */ readonly forming: BarView; /** Runs for every bar of history, once, and again on every update of the forming bar. */ onBar(handler: () => void): void; /** Runs once when a bar closes, after its last `onBar`. */ onClose(handler: () => void): void; } /** The chart's bars by default, or another market or interval. */ declare function bars(options?: BarsOptions): Bars; /** One trade (a taker fill), with the identities 0xArchive records. */ interface Trade { readonly time: Time; readonly price: number; readonly size: number; /** price × size, in USD. */ readonly notional: number; /** The taker's side. */ readonly side: 'buy' | 'sell'; readonly tradeId: string | null; /** The taker's order id: fills with the same id are one order. */ readonly takerOrderId: string | null; /** Wallet addresses, lower-case hex, when the source carries them. */ readonly taker: string | null; readonly maker: string | null; /** True when the taker was being liquidated. */ readonly liquidation: boolean; /** * True when the same taker order filled the trade before this one on the stream (time order, * execution order within a millisecond), filters such as `minSize` aside. False where the * source does not say. Join fills into orders with it when `takerOrderId` is null. */ readonly sameOrder: boolean; /** * The trade's change in the venue's open interest, in the market's base unit, from both sides' * positions before it: positive where it opened positions, negative where it closed them, 0 where * one side opened and the other closed. Hyperliquid and HIP-3 count both sides of open interest, * Lighter longs only, as each venue's own open interest does. `na` where the venue does not know. */ readonly oiDelta: number; } interface TradesOptions { market?: string; venue?: Venue; /** Skip trades smaller than this, in base units or in USD. */ minSize?: number; minNotional?: number; } interface Trades { /** Runs once per trade, in execution order, before the `onBar` of the bar it falls in. */ onTrade(handler: (trade: Trade) => void): void; /** Whether trades cover the current bar completely. Trade history is limited, so older bars have none. */ readonly available: boolean; } declare function trades(options?: TradesOptions): Trades; interface Liquidation { readonly time: Time; readonly price: number; readonly size: number; readonly notional: number; /** The side of the position that was liquidated. */ readonly side: 'long' | 'short'; readonly user: string | null; } interface Liquidations { onLiquidation(handler: (liquidation: Liquidation) => void): void; readonly available: boolean; } declare function liquidations(options?: { market?: string; venue?: Venue; minNotional?: number }): Liquidations; /** Perp state as of the current bar: the last sample at or before it. `na` when unknown. */ interface Derivatives { /** When the sample read was taken; `na` when there is none. */ readonly time: Time; readonly openInterest: number; readonly openInterestUsd: number; /** The funding rate for one hour, as a fraction (0.0001 is 0.01%). */ readonly funding: number; readonly markPrice: number; readonly oraclePrice: number; readonly premium: number; readonly available: boolean; } declare function derivatives(options?: { market?: string; venue?: Venue }): Derivatives; /** The share of a venue's markets trading above their session VWAP, as of the current bar. */ interface Breadth { /** Percent, 0 to 100. */ readonly value: number; readonly eligible: number; /** How complete the sample is: the percent of the venue's markets that had data for it, 0 to 100. */ readonly coverage: number; /** When the sample read was calculated; `na` when there is none. */ readonly time: Time; readonly available: boolean; } declare function breadth(options?: { venue?: 'hl' | 'hip3' }): Breadth; /** Resting size in the book near the price, as of the current update. */ interface Depth { /** Total resting bid and ask size within `bandPct` percent of the mid. `bandPct` must be one of the declared bands. */ bid(bandPct: number): number; ask(bandPct: number): number; readonly bestBid: number; readonly bestAsk: number; readonly mid: number; readonly spread: number; /** When the sample read was taken; `na` when there is none. */ readonly time: Time; /** Book views have no history until the terminal serves one; before that only live bars have them. */ readonly available: boolean; } declare function depth(options: { bandsPct: readonly number[]; market?: string; venue?: Venue }): Depth; /** A band around the mid, in basis points: 10 is 0.1%, 1000 is 10%. */ type BookBand = 10 | 25 | 50 | 100 | 250 | 500 | 1000; /** `avg`, the default: the time-weighted mean over the bar. `close`: the book at the bar's end. */ type BookStat = 'avg' | 'close'; /** * The full-depth order book over each of the chart's bars (Hyperliquid and HIP-3, from 2026-03-10): * from 0xArchive's book archive, and for the newest hour from the page's live book. A bar the book * was not known for reads `na`, never 0. Live, the page holds up to 400 levels a side, so a band * that reaches past them reads `na` until the archive has the hour. */ interface OrderBook { /** Mid price. */ mid(options?: { stat?: BookStat }): number; /** Best ask less best bid over the mid, in basis points. */ spreadBps(options?: { stat?: BookStat }): number; /** Size resting at the best bid and the best ask (the best queues), in contracts. */ bestBidSize(options?: { stat?: BookStat }): number; bestAskSize(options?: { stat?: BookStat }): number; /** Resting size within `band` of the mid on one side: in contracts, or in USD with `usd: true`. */ bid(band: BookBand, options?: { usd?: boolean; stat?: BookStat }): number; ask(band: BookBand, options?: { usd?: boolean; stat?: BookStat }): number; /** Bids less asks over their sum, in USD within `band`: -1 (all asks) to 1 (all bids). */ imbalance(band: BookBand, options?: { stat?: BookStat }): number; /** What a market order of `usd` pays against the mid at the bar's end, in basis points; `na` when the book cannot fill it. */ slippageBuy(usd: 10000 | 100000 | 1000000): number; slippageSell(usd: 10000 | 100000 | 1000000): number; /** Price levels on each side at the bar's end; `na` live. */ readonly bidLevels: number; readonly askLevels: number; /** The share of the bar the book was known, 0 to 1. */ readonly coverage: number; /** True when any of the bar came from the live book rather than the archive. */ readonly live: boolean; /** The bar's open time; `na` when there is no reading. */ readonly time: Time; readonly available: boolean; } declare function orderBook(options?: { market?: string; venue?: Venue }): OrderBook; /** * Open interest over each of the chart's bars, from 0xArchive's one-minute open interest * (Hyperliquid from 2025-03-01, HIP-3 and Lighter from their own starts). A bar opens at the close * of the minute before it, so `close - open` is its whole change; its high and low are over its * minutes. In the market's base unit, or USD with `{ usd: true }` (each minute's open interest times * its mark). * * The newest bars are built from the trades instead: each trade's change in open interest * (`Trade.oiDelta`), summed and anchored to the one-minute open interest, so the forming bar moves * with every trade and each bar closes where its last trade left it. On time bars of a minute or * more that is every bar since the hour before the chart opened; on seconds bars and bars that close * on trades (tick, volume, range and the rest) it is every bar, as far back as the trades reach * (`trades()`: today and the two UTC days before). USD there is each level times the price of the * trade that set it. */ interface OpenInterest { open(options?: { usd?: boolean }): number; high(options?: { usd?: boolean }): number; low(options?: { usd?: boolean }): number; close(options?: { usd?: boolean }): number; /** * The bar's change, close less open. In USD it is the change in coins at the bar's closing mark, * so it counts positions opened and closed, not the price moving the ones already open. */ change(options?: { usd?: boolean }): number; /** The bar's open time; `na` when there is no reading. */ readonly time: Time; readonly available: boolean; } declare function openInterest(options?: { market?: string; venue?: Venue }): OpenInterest; /** * The funding rate over each of the chart's bars, from 0xArchive's one-minute funding readings * (Hyperliquid from 2025-03-01, HIP-3 and Lighter from their own starts). A bar's rate is the * venue's rate at its last minute: on Hyperliquid the predicted rate for the hour then running, * which is what settles at the hour's end, so a 1h bar holds the rate its hour paid. Positive: * longs pay shorts. The forming bar moves as each minute closes, about 1 to 4 minutes behind. Only * time charts of whole minutes, 1m or longer, have it: on seconds, tick, volume and range bars * every value is `na`. */ interface Funding { /** The rate as a fraction per hour, as the venues pay it, or per `hours` hours (8 for an eight-hour rate, 8760 for a year's). */ rate(options?: { hours?: number }): number; /** The premium at the bar's last minute, the mark's distance from the oracle the rate is built from; `na` on Lighter. */ readonly premium: number; /** The bar's open time; `na` when there is no reading. */ readonly time: Time; readonly available: boolean; } declare function funding(options?: { market?: string; venue?: Venue }): Funding; /** A price level with resting size on each side, such as a liquidation or stop cluster. */ interface Level { readonly price: number; /** USD notional that would trade if price reached the level: long and short positions, or buy and sell orders. */ readonly longUsd: number; readonly shortUsd: number; } interface Levels { /** The levels as of the current bar, sorted by price. Empty when unknown. */ levels(): readonly Level[]; /** When the snapshot read was taken; `na` when there is none. */ readonly time: Time; readonly available: boolean; } /** Where open positions would be liquidated, from 0xArchive's liquidation levels. */ declare function liquidationLevels(options?: { market?: string; venue?: Venue }): Levels; /** Resting stop and trigger orders by price, where the data exists. */ declare function triggers(options?: { market?: string; venue?: Venue }): Levels; /** Exchange session calendars for HIP-3 stock and commodity perps (src/browser/market-calendars.mjs). */ declare const sessions: { /** The calendar id for a market, such as `nyse`, or `null` for markets that trade around the clock. */ calendarOf(market: string): string | null; /** * Whether `calendar` is in session at `time` (regular or extended hours), with that session's * bounds; when closed, `start` is the last close and `end` the next open. `regular` says whether * the session is the regular one. Holidays and early closes included. */ at(calendar: string, time: Time): { readonly open: boolean; readonly regular: boolean; readonly start: Time; readonly end: Time }; }; // --------------------------------------------------------------------------------------------- // State: values that persist between runs. Declared at the top level only. // // A local variable inside a handler starts fresh on every run. Anything that must persist is // declared here, with its lifetime, so the engine can roll it back correctly. // --------------------------------------------------------------------------------------------- type StateValue = number | boolean | string | null; interface Cell { value: T; } /** A number with history, made with `series(lookback)`. */ interface Series { /** Sets the value for the current bar. Unset bars are `na`. */ set(value: number): void; /** The current bar's value. */ readonly value: number; /** The value `n` bars ago, `n` at most the declared lookback. */ ago(n: number): number; } interface Tally { add(amount: number): void; /** The sum so far in the current bar. */ readonly value: number; /** How many amounts were added in the current bar. */ readonly count: number; } interface TallyBy { /** Add to `key`'s sum for this bar. A new key past the capacity is dropped for the rest of the bar. */ add(key: string, amount: number): void; /** `key`'s sum so far in this bar, 0 when it has none. */ get(key: string): number; /** How many keys have a sum this bar. */ readonly size: number; /** Every key with its sum, in the order the keys were first added. */ entries(): IterableIterator<[string, number]>; } interface StateList { readonly length: number; /** The value at `index`, from 0 at the oldest. */ get(index: number): T; set(index: number, value: T): void; /** Add at the end. Past the capacity the script stops (OX2004): `shift` the oldest first. */ push(value: T): void; /** Take from the end. */ pop(): T; /** Take from the start, the oldest. */ shift(): T; /** Add at the start. */ unshift(value: T): void; clear(): void; /** A copy as a plain array, oldest first. */ toArray(): T[]; } interface StateMap { readonly size: number; /** The value for `key`, or `undefined` when there is none. */ get(key: string): T | undefined; /** Set the value for `key`. A new key past the capacity stops the script (OX2004). */ set(key: string, value: T): void; /** Remove `key`; true when it was there. */ delete(key: string): boolean; has(key: string): boolean; clear(): void; entries(): IterableIterator<[string, T]>; } type Lifetime = 'bar' | 'live'; declare const state: { /** Kept across bars; rolled back before each update of the forming bar (Pine's `var`). */ bar(initial: T): Cell; /** Kept across bars and across updates of the forming bar (Pine's `varip`). */ live(initial: T): Cell; /** A list of at most `capacity` values with a lifetime, default `'bar'`. */ list(capacity: number, lifetime?: Lifetime): StateList; map(capacity: number, lifetime?: Lifetime): StateMap; }; /** A number with history: `lookback` bars kept, at most 5,000. */ declare function series(lookback: number): Series; /** A sum of what event handlers add during a bar. Starts at 0 on each new bar; never rolled back. */ declare function tally(): Tally; /** A sum per key (such as a taker order id) during a bar, at most `capacity` keys. Empty on each new bar. */ declare function tallyBy(capacity: number): TallyBy; // --------------------------------------------------------------------------------------------- // Indicators: instances with their own state. Create at the top level, update in a handler. // // Update an instance once per run, on every bar: an instance only advances when `update` runs, // so updating inside an `if` gives a different result (Pine's call-site rule, made visible). // Values match TradingView's built-ins, including their seeds and how they treat `na`. // --------------------------------------------------------------------------------------------- interface Indicator { update(...values: In): Out; /** The value after the latest update in this run. */ readonly value: Out; /** The value as of this stream's last closed bar. Read it from other streams' handlers. */ readonly confirmed: Out; } type Average = Indicator<[value: number], number>; interface MacdValue { readonly macd: number; readonly signal: number; readonly histogram: number } interface BandsValue { readonly basis: number; readonly upper: number; readonly lower: number } interface DmiValue { readonly plus: number; readonly minus: number; readonly adx: number } interface SupertrendValue { readonly line: number; /** 1 up, -1 down */ readonly direction: number } type VwapAnchor = 'session' | 'week' | 'month' | 'quarter' | 'year' | Time; declare const ta: { /** Simple moving average of the last `length` values. */ sma(length: number): Average; /** Seeded with the simple average of the first `length` values, as TradingView's `ta.ema`. */ ema(length: number): Average; /** Wilder's average, seeded like `ema`. */ rma(length: number): Average; /** Linearly weighted moving average: the newest value weighs `length`, the oldest 1. */ wma(length: number): Average; /** Hull moving average, a weighted average of weighted averages that lags less. */ hma(length: number): Average; /** Volume-weighted moving average of the last `length` values. */ vwma(length: number): Indicator<[value: number, volume: number], number>; /** Standard deviation of the last `length` values, over `length` as TradingView divides (not `length - 1`). */ stdev(length: number): Average; /** Variance of the last `length` values, the square of `stdev`. */ variance(length: number): Average; /** The highest of the last `length` values, the current one included. */ highest(length: number): Average; /** The lowest of the last `length` values, the current one included. */ lowest(length: number): Average; /** Bars since the highest or lowest value in the window, as a negative offset like TradingView's. */ highestBars(length: number): Average; lowestBars(length: number): Average; /** value − value `length` bars ago. */ change(length?: number): Average; /** Momentum: the value less the value `length` bars ago. */ mom(length: number): Average; /** Rate of change: the percent change from the value `length` bars ago. */ roc(length: number): Average; /** Relative strength index, 0 to 100, on Wilder's averages. */ rsi(length: number): Average; /** Commodity channel index: the distance from the `length`-bar average in units of mean deviation, over 0.015. */ cci(length: number): Average; /** The least-squares line through the last `length` values, read `offset` bars back from its end (0, the default, is the current bar). */ linreg(length: number, offset?: number): Average; /** The percent of the previous `length` values that are at or below the current one. */ percentRank(length: number): Average; /** The running sum of every value since the first bar. */ cum(): Average; /** True when the value has risen on each of the last `length` bars (a step within 1e-10 does not count). */ rising(length: number): Indicator<[value: number], boolean>; /** True when the value has fallen on each of the last `length` bars. */ falling(length: number): Indicator<[value: number], boolean>; /** True on the bar where `a` goes from at or below `b` to above it. */ crossover(): Indicator<[a: number, b: number], boolean>; /** True on the bar where `a` goes from at or above `b` to below it. */ crossunder(): Indicator<[a: number, b: number], boolean>; /** True on the bar where `a` crosses `b` either way. */ cross(): Indicator<[a: number, b: number], boolean>; /** Bars since `condition` was last true: 0 on a bar where it is, `na` until the first time. */ barsSince(): Indicator<[condition: boolean], number>; /** `value` on the bar where `condition` was true, the `occurrence`-th time back (0, the default, is the latest). */ valueWhen(occurrence?: number): Indicator<[condition: boolean, value: number], number>; /** * A pivot high: a value higher than the `left` before it and the `right` after it. It is known * only `right` bars later, so it is returned on that bar, `na` on every other. */ pivotHigh(left: number, right: number): Indicator<[value: number], number>; /** A pivot low, as `pivotHigh`: returned `right` bars after the low, `na` on every other bar. */ pivotLow(left: number, right: number): Indicator<[value: number], number>; /** MACD: the fast EMA less the slow EMA, its `signal`-bar EMA, and the difference of the two as the histogram. */ macd(fast: number, slow: number, signal: number): Indicator<[value: number], MacdValue>; /** Bollinger Bands: the `length`-bar SMA, and bands `mult` standard deviations either side. */ bb(length: number, mult: number): Indicator<[value: number], BandsValue>; /** Keltner Channels: the `length`-bar EMA of the close, and bands `mult` times the `length`-bar EMA of the true range (or of the bar's range) either side. */ kc(length: number, mult: number, useTrueRange?: boolean): Indicator<[bar: BarView], BandsValue>; /** Stochastic: where the close sits in the `length`-bar range of highs and lows, 0 to 100. Smooth it with `ta.sma` for %K and %D. */ stoch(length: number): Indicator<[close: number, high: number, low: number], number>; /** Money flow index, 0 to 100: an RSI of the typical price weighted by volume. */ mfi(length: number): Indicator<[bar: BarView], number>; /** On-balance volume: the running sum of volume, added on up closes and taken away on down closes. */ obv(): Indicator<[bar: BarView], number>; /** True range. `handleNa` as TradingView: on the first bar, high − low. */ tr(handleNa?: boolean): Indicator<[bar: BarView], number>; /** Average true range: Wilder's average of the true range over `length` bars. */ atr(length: number): Indicator<[bar: BarView], number>; /** Directional movement: +DI, -DI and the ADX, which measures trend strength whatever its direction. */ dmi(diLength: number, adxSmoothing: number): Indicator<[bar: BarView], DmiValue>; /** Supertrend: a trailing line `factor` ATRs from the price that flips side when the close crosses it, with its direction. */ supertrend(factor: number, atrLength: number): Indicator<[bar: BarView], SupertrendValue>; /** Volume-weighted average price from quote and base volume, reset at each anchor (UTC). */ vwap(anchor?: VwapAnchor): Indicator<[bar: BarView], number>; }; // --------------------------------------------------------------------------------------------- // Outputs. Declared at the top level only, at most 64 plots, set in handlers. // A plot not set on a bar is `na` there: a line or an area breaks (a gap), and a plot that says // `join: true` is drawn straight across the bars it has no value on. Histograms and points draw // nothing on such a bar. // --------------------------------------------------------------------------------------------- interface PlotOptions { color?: Color | null; pane?: PaneName; /** Hide from the chart but keep in the legend and the data window. */ hidden?: boolean; precision?: number; /** * Show the last value as a tag on the price axis, without the line across the chart that * `trackPrice` adds. Default: false on the price pane, true in the script's own pane. */ axisLabel?: boolean; /** The scale margins of the scale this plot sits on, as fractions of the pane, such as `{ top: 0.2, bottom: 0.28 }`. */ margins?: { top: number; bottom: number }; /** * Draw on a hidden scale of its own inside the pane, between these fractions from the top and * the bottom, such as volume at the foot of the price pane: `{ top: 0.84, bottom: 0 }`. */ band?: { top: number; bottom: number }; /** A fixed scale from `min` to `max` instead of autoscaling, such as `{ min: 0, max: 100 }`. */ range?: { min: number; max: number }; /** * `false`: the plot is drawn at its values but does not stretch its scale, which fits the rest * of the pane (the candles, on the price pane). For levels far from price, such as liquidation * clusters, that would otherwise squash the candles into a line; parts outside the view are * simply not seen. Default true. */ autoscale?: boolean; /** * Draw the plot shifted by this many bars, a whole number from -500 to 500. The value set on * bar `i` is drawn on bar `i + offset`: a positive offset shifts the plot right, and its last * values are drawn past the newest bar, in the empty space to its right (Ichimoku's leading * spans); a negative one shifts it left (the lagging span). The legend reads the value drawn * at the bar under the crosshair. Default 0. */ offset?: number; } interface JoinOptions { /** * Draw the line (and the area's fill) across bars where the plot is `na`, from the last value * to the next. Default false: the plot breaks there, as Pine's `plot.style_linebr` and * `style_areabr` do. Pine's `style_line`, `style_stepline` and `style_area` join, and the * translation of a pasted Pine script says so. */ join?: boolean; } interface LinePlotOptions extends PlotOptions, JoinOptions { width?: LineWidth; style?: LineStyle; /** Show the last value as a line across the chart and a tag on the price axis. */ trackPrice?: boolean; } interface Plot { /** The value for the current bar, optionally with this bar's colour. */ set(value: number, color?: Color | null): void; } /** * A horizontal line across the whole pane (`plot.level`). It can be one side of a `plot.fill`. * `set` moves it: the line is drawn at the price in effect on the newest bar, across every bar. */ interface LevelPlot { set(price: number): void; } interface ShapePlot { /** * Draw the shape on the current bar when `condition` is true. `price` is used with * `location: 'absolute'`; `text` and `color` replace the plot's own for this bar. */ set(condition: boolean, options?: { price?: number; text?: string; color?: Color | null; textColor?: Color | null }): void; } interface ShapeOptions { /** The shape to draw. Give `char` instead to draw a character (Pine's `plotchar`). */ shape?: ShapeName; /** One character drawn in place of a shape, such as `'▲'` or `'•'`. */ char?: string; location: ShapeLocation; color?: Color | null; size?: Size; /** Written beside the shape, or inside a `labelUp` / `labelDown` label. */ text?: string; /** The text's colour. Default: the shape's colour; inside a label, black or white, whichever reads on it. */ textColor?: Color | null; pane?: PaneName; /** Kept in the declaration but not drawn, like a plot's `hidden`. */ hidden?: boolean; /** Shift the shapes this many bars, as `PlotOptions.offset`. */ offset?: number; } interface Marker { position: 'above' | 'below' | 'at'; shape: ShapeName; price?: number; text?: string; color?: Color | null; } interface MarkersPlot { /** Add a marker to the current bar. Several per bar are allowed. */ add(marker: Marker): void; } interface ColorPlot { /** The colour for the current bar; `null` for none. */ set(color: Color | null): void; } interface CandlePlot { set(bar: { open: number; high: number; low: number; close: number }, color?: Color | null): void; } declare const plot: { line(title: string, options?: LinePlotOptions): Plot; histogram(title: string, options?: PlotOptions & { base?: number }): Plot; area(title: string, options?: PlotOptions & JoinOptions): Plot; /** Dots or crosses at each value, with no line between them. */ points(title: string, options?: PlotOptions & { shape?: 'circle' | 'cross' }): Plot; /** A shape, a label or a character on the bars where `set` says so. */ shape(title: string, options: ShapeOptions): ShapePlot; markers(title: string, options?: { pane?: PaneName }): MarkersPlot; /** * Shade between two plots or levels of this script, optionally per bar. Between two levels it * is a band across the whole pane. A fill takes the offset of its first plot, colours included. */ fill(title: string, a: Plot | LevelPlot, b: Plot | LevelPlot, options?: { color?: Color | null }): ColorPlot; /** Shade the whole height of the pane behind a bar. */ background(title: string, options?: { pane?: PaneName; offset?: number }): ColorPlot; /** A horizontal line across the pane, such as 70 on an RSI. Returns a handle a fill can use. */ level(title: string, price: number, options?: { color?: Color | null; style?: LineStyle; width?: LineWidth; pane?: PaneName; hidden?: boolean }): LevelPlot; candles(title: string, options?: PlotOptions): CandlePlot; /** * Recolour the chart's own candles, bodies, borders and wicks, as Pine's `barcolor` does. * `null` leaves a bar in the chart's colours. Chart types without candles (footprints) say in * the legend that it does not apply there. */ barColor(title: string, options?: { offset?: number }): ColorPlot; }; // --------------------------------------------------------------------------------------------- // Drawings: objects placed at times and prices, capped per kind (see IndicatorOptions). // Create them in handlers. Changes made while the forming bar updates are rolled back with it. // --------------------------------------------------------------------------------------------- /** A point by time, or by bar number on the chart. Up to 500 bars into the future. */ type Point = { time: Time; price: number } | { bar: number; price: number }; interface LineStyleOptions { color?: Color | null; width?: LineWidth; style?: LineStyle; extend?: 'none' | 'left' | 'right' | 'both'; } interface Drawing { /** Stable while the drawing exists, including across updates of the forming bar. */ readonly id: number; update(options: Partial): void; delete(): void; } interface LineOptions extends LineStyleOptions { from: Point; to: Point } interface BoxOptions { from: Point; to: Point; border?: LineStyleOptions; fill?: Color | null; text?: string; textColor?: Color | null } /** * How a label sits at its point. `up`, `down`, `left` and `right` are boxes with a pointer at the * point (an `up` label sits below it and points up); the corner styles put the box's corner on * the point; `center` centres the box on it; `none` is the text alone. A shape name draws that * shape at the point with the text above it. */ type LabelStyle = 'up' | 'down' | 'left' | 'right' | 'upperLeft' | 'upperRight' | 'lowerLeft' | 'lowerRight' | 'center' | 'none' | ShapeName; interface LabelOptions { at: Point; text: string; style?: LabelStyle; color?: Color | null; textColor?: Color | null; size?: Size; tooltip?: string } interface PolylineOptions extends LineStyleOptions { points: readonly Point[]; closed?: boolean; fill?: Color | null } interface LinefillOptions { color?: Color | null; } type TablePosition = 'top-left' | 'top-center' | 'top-right' | 'middle-left' | 'middle-center' | 'middle-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; interface TableOptions { /** Where the table sits in the script's pane, clear of the legend and the price axis. */ position: TablePosition; columns: number; rows: number; background?: Color | null; /** The lines between cells, and their width in pixels (0 to 4, default 1). */ border?: Color | null; borderWidth?: number; /** The outline around the table, and its width in pixels (0 to 4, default 1). */ frame?: Color | null; frameWidth?: number; } interface TableCellOptions { color?: Color | null; background?: Color | null; size?: Size; align?: 'left' | 'center' | 'right'; valign?: 'top' | 'middle' | 'bottom'; /** The cell's width as a percent of the pane's width, 0 to 100. 0, the default, fits the text. */ width?: number; /** The cell's height as a percent of the pane's height, 0 to 100. 0, the default, fits the text. */ height?: number; /** Shown when the pointer rests on the cell. Text only, like the cell. */ tooltip?: string; } interface Table { readonly id: number; /** Text only; no markup, links or images. */ cell(column: number, row: number, text: string, options?: TableCellOptions): void; /** * Join the cells from (`fromColumn`, `fromRow`) to (`toColumn`, `toRow`) into one, which shows * the first cell's text and style. Merged areas may not overlap; a later merge that overlaps an * earlier one replaces it. */ merge(fromColumn: number, fromRow: number, toColumn: number, toRow: number): void; clear(): void; delete(): void; } /** Drawings are made in the chart's handlers; a table may also be made at the top level. */ declare const draw: { line(options: LineOptions): Drawing; box(options: BoxOptions): Drawing; /** Labels are text only; no markup, links or images. */ label(options: LabelOptions): Drawing; polyline(options: PolylineOptions): Drawing; /** * Shade between two lines of this script, as drawn (extensions included). The fill goes when * either line is deleted. */ linefill(a: Drawing, b: Drawing, options?: LinefillOptions): Drawing; /** At most 9 tables. */ table(options: TableOptions): Table; }; // --------------------------------------------------------------------------------------------- // Alerts: declared at the top level, armed by the user, fired from handlers. // In v1 they fire in the browser while the chart is open. // --------------------------------------------------------------------------------------------- interface Alert { /** Fire if the user armed this alert and `condition` is true. At most once per bar. */ fire(condition: boolean, message?: string): void; } declare function alert(name: string, options?: { message?: string; /** Also fire on updates of the forming bar, not only when a bar closes. Repaints. */ onForming?: boolean; }): Alert; // --------------------------------------------------------------------------------------------- // Colours, formatting, time, logging, randomness // --------------------------------------------------------------------------------------------- declare const color: { /** The theme's up and down colours, and a neutral text colour. */ readonly up: Color; readonly down: Color; readonly neutral: Color; /** The chart's own background, for a table or label that should sit on the theme's surface. */ readonly panel: Color; /** A colour that draws nothing: a transparent line segment, bar or shade. */ readonly none: Color; /** The theme's study palette, `i` from 0 to 15. */ study(i: number): Color; /** A colour from red, green and blue, 0 to 255, and `alpha` from 0 to 1. */ rgb(r: number, g: number, b: number, alpha?: number): Color; /** The same colour with `alpha` from 0 (transparent) to 1: `#rrggbbaa` for a hex colour, `theme:up@0.3` for a token. */ withAlpha(c: Color, alpha: number): Color; /** A colour between two hex colours, `t` from 0 to 1. A theme token cannot be mixed: `a` comes back. */ mix(a: Color, b: Color, t: number): Color; }; declare const fmt: { /** A price in the market's own precision. */ price(value: number): string; /** A number with a comma every three digits and `decimals` places, as the terminal writes numbers whatever the reader's locale. */ num(value: number, decimals?: number): string; /** `$1.2M`, `$350k`. */ usd(value: number): string; /** A fraction as a percent: `pct(0.0123)` is `1.23%`. */ pct(value: number, decimals?: number): string; /** A number shortened with a suffix, `1.2M` or `350k`, as the axes of a script's own pane print. */ compact(value: number): string; }; interface TimeParts { readonly year: number; readonly month: number; readonly day: number; readonly hour: number; readonly minute: number; readonly second: number; /** 0 Sunday to 6 Saturday. */ readonly dayOfWeek: number; } declare const clock: { /** Calendar fields of `time` in `timeZone` (IANA name), UTC by default. */ parts(time: Time, timeZone?: string): TimeParts; /** The start of the day, week (Monday), month, quarter or year that contains `time`. */ startOf(time: Time, unit: 'day' | 'week' | 'month' | 'quarter' | 'year', timeZone?: string): Time; }; /** Lines in the script's console, each with the time of the bar it was written on; 1,000 at most, oldest dropped. */ declare const log: { info(...values: unknown[]): void; warn(...values: unknown[]): void; error(...values: unknown[]): void; }; /** A seeded random number generator: the same seed gives the same numbers everywhere. */ declare function random(seed: number): { next(): number }; ``` ## Helpers for code the Pine front end generates: pine-compat.d.ts ```ts /** * The `pine` namespace: helpers only code generated by the Pine front end needs. Native scripts * may use them but have no reason to. The Pine front end (src/script/pine/) owns this file and * implements it (src/script/pine/runtime/index.ts); the engine installs it as a global beside the * rest of the API. * * Pine has values our API does not: arrays, maps and matrices, user-defined types, drawings it * reads back, and history of any value. They live in the `pine` namespace, and every change to * them is journaled so it rolls back with the forming bar like the engine's own state: each * handler that touches them begins with `heap.begin()` on a heap made at setup. */ /** Pine's array. Indexes from the end work (`-1` is the last element); an index out of range is an error. */ interface PineArray { readonly items: readonly T[] } /** Pine's map. A missing key reads as na. */ interface PineMap { readonly entries: ReadonlyMap } /** Pine's matrix. */ interface PineMatrix { readonly rows: readonly (readonly T[])[] } /** A Pine drawing: its Pine properties, drawn with `draw.*`. `null` is na. */ interface PineLine { readonly kind: 'line' } interface PineLabel { readonly kind: 'label' } interface PineBox { readonly kind: 'box' } interface PineTable { readonly kind: 'table' } interface PinePolyline { readonly kind: 'polyline' } interface PineLinefill { readonly kind: 'linefill' } /** Pine's `chart.point`. */ interface PinePoint { time: number; index: number; price: number } /** A bar's volume footprint (`request.footprint`), from taker trades. `null` is na. */ interface PineFootprint { readonly kind: 'footprint' } /** One row of a footprint. `null` is na. */ interface PineVolumeRow { readonly kind: 'volume_row' } /** `request.security_lower_tf`: a value per lower-timeframe bar. */ interface PineIntrabars { /** Sets the value on lower bar `index`, which opens at `time`. */ set(value: T, index: number, time: number): void; /** The values of the lower bars opening in [from, to), oldest first; `null` (na) when the timeframe is not lower and was to be ignored. */ within(from: number, to: number): PineArray; } /** The journal of one handler's Pine objects. */ interface PineHeap { /** Call first in every run of the handler: undoes the changes of a run the engine rolled back. */ begin(): void; } /** History (`x[n]`) of a value that is not a number, keyed by bar number. */ interface PineHistory { set(value: T, index: number): void; /** The value `n` bars before bar `index`; `null` (na) when not kept. */ ago(n: number, index: number): T; /** Sets the value for bar `index` and reads `n` bars back. */ at(value: T, n: number, index: number): T; } /** A Pine user-defined type. Objects are plain objects whose field changes are journaled. */ interface PineType { /** `X.new(...)`: fields not given take the declared defaults. */ 'new'(fields?: Partial): T; copy(obj: T): T; } interface PineBands { readonly basis: number; readonly upper: number; readonly lower: number } interface PineLineOptions { first_point?: PinePoint; second_point?: PinePoint; x1?: number; y1?: number; x2?: number; y2?: number; xloc?: string; extend?: string; color?: Color | null; style?: string; width?: number; force_overlay?: boolean } interface PineLabelOptions { point?: PinePoint; x?: number; y?: number; text?: string; xloc?: string; yloc?: string; color?: Color | null; style?: string; textcolor?: Color | null; size?: string | number; textalign?: string; tooltip?: string; text_font_family?: string; force_overlay?: boolean; text_formatting?: string } interface PineBoxOptions { top_left?: PinePoint; bottom_right?: PinePoint; left?: number; top?: number; right?: number; bottom?: number; border_color?: Color | null; border_width?: number; border_style?: string; extend?: string; xloc?: string; bgcolor?: Color | null; text?: string; text_size?: string | number; text_color?: Color | null; text_halign?: string; text_valign?: string; text_wrap?: string; text_font_family?: string; force_overlay?: boolean; text_formatting?: string } interface PineTableOptions { position: string; columns: number; rows: number; bgcolor?: Color | null; frame_color?: Color | null; frame_width?: number; border_color?: Color | null; border_width?: number; force_overlay?: boolean } interface PineCellOptions { table_id: PineTable; column: number; row: number; text?: string; width?: number; height?: number; text_color?: Color | null; text_halign?: string; text_valign?: string; text_size?: string | number; bgcolor?: Color | null; tooltip?: string; text_font_family?: string; text_formatting?: string } interface PinePolylineOptions { points: PineArray; curved?: boolean; closed?: boolean; xloc?: string; line_color?: Color | null; fill_color?: Color | null; line_style?: string; line_width?: number; force_overlay?: boolean } declare const pine: { /** * For `barmerge.lookahead_on` on history bars: the bar of `stream` that contains the current * chart bar, with its final values. It looks ahead, which is why the public API has no such read. * On live bars it is the same as `stream.forming`. */ containing(stream: Bars): BarView; // --- state Pine has and our API does not --------------------------------------------------- /** A handler's journal, on a `state.bar(0)` revision cell. Drawing caps as the declaration's. */ heap(rev: Cell, caps?: { lines?: number; labels?: number; boxes?: number; polylines?: number }): PineHeap; /** `var x = `: a cell rolled back with the bar. */ bar(initial: T): Cell; /** `varip x = `: a cell never rolled back. */ live(initial: T): Cell; history(lookback: number): PineHistory; /** `expr[n]`: records `value` in `series` and reads it `n` bars back. */ at(series: Series, value: number, n: number): number; /** `request.security(..., expr[k], lookahead_on)`: `now` if the request last ran on the bar containing this one, else `next`. */ lookahead(time: number, containingTime: number, now: T, next: T): T; /** `gaps = barmerge.gaps_on`: `value` only on the chart bar where a new requested bar arrived, else `na`. */ gaps(value: T, time: number, seen: Cell, na?: T): T; /** A Pine ticker (`BINANCE:BTCUSDT`) as our market for `bars({ market })`; `undefined` is the chart's. */ market(ticker: string | null): string | undefined; /** `x.copy()` of a user-defined type's object. */ copy(obj: T): T; type(defaults: T): PineType; /** An enum input's chosen title back to the member's name. */ enumValue(title: string, members: readonly string[], titles: readonly string[]): string; // --- collections --------------------------------------------------------------------------- array: { make(size?: number, initial?: T): PineArray; from(...values: T[]): PineArray; get(a: PineArray, index: number): T; set(a: PineArray, index: number, value: T): void; push(a: PineArray, value: T): void; pop(a: PineArray): T; shift(a: PineArray): T; unshift(a: PineArray, value: T): void; insert(a: PineArray, index: number, value: T): void; remove(a: PineArray, index: number): T; clear(a: PineArray): void; size(a: PineArray): number; first(a: PineArray): T; last(a: PineArray): T; includes(a: PineArray, value: T): boolean; indexof(a: PineArray, value: T): number; lastindexof(a: PineArray, value: T): number; max(a: PineArray, nth?: number): number; min(a: PineArray, nth?: number): number; sum(a: PineArray): number; avg(a: PineArray): number; median(a: PineArray): number; mode(a: PineArray): number; range(a: PineArray): number; stdev(a: PineArray, biased?: boolean): number; variance(a: PineArray, biased?: boolean): number; covariance(a: PineArray, b: PineArray, biased?: boolean): number; standardize(a: PineArray): PineArray; abs(a: PineArray): PineArray; copy(a: PineArray): PineArray; slice(a: PineArray, from: number, to?: number): PineArray; concat(a: PineArray, b: PineArray): PineArray; reverse(a: PineArray): void; sort(a: PineArray, order?: string): void; sortIndices(a: PineArray, order?: string): PineArray; fill(a: PineArray, value: T, from?: number, to?: number): void; join(a: PineArray, separator?: string): string; every(a: PineArray): boolean; some(a: PineArray): boolean; binarySearch(a: PineArray, value: number): number; binarySearchLeftmost(a: PineArray, value: number): number; binarySearchRightmost(a: PineArray, value: number): number; percentileLinearInterpolation(a: PineArray, percentage: number): number; percentileNearestRank(a: PineArray, percentage: number): number; percentrank(a: PineArray, index: number): number; /** For `for x in a`: a snapshot of the elements. */ values(a: PineArray): T[]; /** For `for [i, x] in a`. */ entries(a: PineArray): [number, T][]; }; map: { /** A new map; `na` is what a missing key reads as. */ make(na: V): PineMap; put(m: PineMap, key: K, value: V): V; get(m: PineMap, key: K): V; contains(m: PineMap, key: K): boolean; remove(m: PineMap, key: K): V; keys(m: PineMap): PineArray; values(m: PineMap): PineArray; size(m: PineMap): number; clear(m: PineMap): void; copy(m: PineMap): PineMap; putAll(m: PineMap, other: PineMap): void; entries(m: PineMap): [K, V][]; }; matrix: { make(rows?: number, columns?: number, initial?: T, na?: T): PineMatrix; get(m: PineMatrix, row: number, column: number): T; set(m: PineMatrix, row: number, column: number, value: T): void; rows(m: PineMatrix): number; columns(m: PineMatrix): number; elementsCount(m: PineMatrix): number; addRow(m: PineMatrix, row?: number, values?: PineArray): void; addCol(m: PineMatrix, column?: number, values?: PineArray): void; removeRow(m: PineMatrix, row?: number): PineArray; removeCol(m: PineMatrix, column?: number): PineArray; row(m: PineMatrix, row: number): PineArray; col(m: PineMatrix, column: number): PineArray; fill(m: PineMatrix, value: T, fromRow?: number, toRow?: number, fromColumn?: number, toColumn?: number): void; copy(m: PineMatrix): PineMatrix; submatrix(m: PineMatrix, fromRow?: number, toRow?: number, fromColumn?: number, toColumn?: number): PineMatrix; reshape(m: PineMatrix, rows: number, columns: number): void; reverse(m: PineMatrix): void; transpose(m: PineMatrix): PineMatrix; swapRows(m: PineMatrix, a: number, b: number): void; swapColumns(m: PineMatrix, a: number, b: number): void; sum(a: PineMatrix, b: PineMatrix | number): PineMatrix; diff(a: PineMatrix, b: PineMatrix | number): PineMatrix; mult(a: PineMatrix, b: PineMatrix | number): PineMatrix; concat(a: PineMatrix, b: PineMatrix): PineMatrix; avg(m: PineMatrix): number; min(m: PineMatrix): number; max(m: PineMatrix): number; median(m: PineMatrix): number; mode(m: PineMatrix): number; trace(m: PineMatrix): number; det(m: PineMatrix): number; inv(m: PineMatrix): PineMatrix; pinv(m: PineMatrix): PineMatrix; rank(m: PineMatrix): number; pow(m: PineMatrix, power: number): PineMatrix; sort(m: PineMatrix, column?: number, order?: string): void; isSquare(m: PineMatrix): boolean; isZero(m: PineMatrix): boolean; isIdentity(m: PineMatrix): boolean; isSymmetric(m: PineMatrix): boolean; isDiagonal(m: PineMatrix): boolean; isBinary(m: PineMatrix): boolean; rowArrays(m: PineMatrix): PineArray[]; rowEntries(m: PineMatrix): [number, PineArray][]; }; // --- strings, colours, maths --------------------------------------------------------------- str: { /** Pine's `str.tostring`; `tick` is the market's tick size, for `format.mintick`. */ tostring(value: unknown, format?: string, tick?: number): string; tonumber(s: string): number; /** Java MessageFormat patterns, as Pine: `{0}`, `{0,number,#.##}`, `{0,date,yyyy-MM-dd}`. */ format(format: string, ...args: unknown[]): string; formatTime(time: number, format?: string, timezone?: string): string; length(s: string): number; lower(s: string): string; upper(s: string): string; trim(s: string): string; contains(s: string, part: string): boolean; startsWith(s: string, part: string): boolean; endsWith(s: string, part: string): boolean; pos(s: string, part: string): number; match(s: string, regex: string): string; replace(s: string, target: string, replacement: string, occurrence?: number): string; replaceAll(s: string, target: string, replacement: string): string; split(s: string, separator: string): PineArray; substring(s: string, begin: number, end?: number): string; repeat(s: string, count: number, separator?: string): string; }; /** Colours with Pine's transparency, 0 (opaque) to 100. */ color: { 'new'(c: Color | null, transp?: number): Color; rgb(r: number, g: number, b: number, transp?: number): Color; r(c: Color | null): number; g(c: Color | null): number; b(c: Color | null): number; t(c: Color | null): number; fromGradient(value: number, bottom: number, top: number, bottomColor: Color | null, topColor: Color | null): Color; }; math: { round(x: number, precision?: number): number; avg(...xs: number[]): number; toDegrees(radians: number): number; toRadians(degrees: number): number; roundToMintick(x: number, tick: number): number; random(rng: { next(): number }, min?: number, max?: number): number; }; // --- Pine built-ins our `ta` has not, or has with other conventions ------------------------ ta: { sum(h: Series, source: number, length: number): number; mfi(hSource: Series, hVolume: Series, source: number, volume: number, length: number): number; alma(h: Series, source: number, length: number, offset: number, sigma: number, floor?: boolean): number; cmo(h: Series, source: number, length: number): number; cog(h: Series, source: number, length: number): number; correlation(h1: Series, h2: Series, a: number, b: number, length: number): number; dev(h: Series, source: number, length: number): number; median(h: Series, source: number, length: number): number; mode(h: Series, source: number, length: number): number; percentileLinear(h: Series, source: number, length: number, percentage: number): number; percentileNearest(h: Series, source: number, length: number, percentage: number): number; rci(h: Series, source: number, length: number): number; swma(h: Series, source: number): number; stdev(h: Series, source: number, length: number, biased?: boolean): number; variance(h: Series, source: number, length: number, biased?: boolean): number; max(s: StateList, source: number): number; min(s: StateList, source: number): number; fixnan(s: StateList, source: number): number; sar(s: StateList, bars: Bars, start: number, inc: number, max: number): number; nvi(s: StateList, bars: Bars): number; pvi(s: StateList, bars: Bars): number; tfChange(s: StateList, bars: Bars, timeframe: string): boolean; /** TradingView's VWAP (source × volume over volume), anchored at each UTC day or at `anchor`. */ vwap(s: StateList, bars: Bars, source: number, anchor?: boolean): number; vwapBands(s: StateList, bars: Bars, source: number, anchor: boolean | undefined, mult: number): [number, number, number]; changed(diff: number): boolean; keltner(basis: number, range: number, mult: number): PineBands; keltnerWidth(k: PineBands): number; bbw(bb: PineBands): number; wpr(highest: number, lowest: number, close: number): number; iii(bars: Bars): number; wvad(bars: Bars): number; accdistValue(bars: Bars): number; pvtValue(bars: Bars): number; wadValue(bars: Bars): number; }; // --- time, sessions, timeframes, symbols --------------------------------------------------- time: { time(bars: Bars, timeframe?: string, session?: string, timezone?: string, barsBack?: number, timeframeBarsBack?: number): number; timeClose(bars: Bars, timeframe?: string, session?: string, timezone?: string, barsBack?: number, timeframeBarsBack?: number): number; timestamp(...args: (string | number)[]): number; year(time: number, timezone?: string): number; month(time: number, timezone?: string): number; dayofmonth(time: number, timezone?: string): number; /** 1 Sunday to 7 Saturday, as Pine. */ dayofweek(time: number, timezone?: string): number; hour(time: number, timezone?: string): number; minute(time: number, timezone?: string): number; second(time: number, timezone?: string): number; weekofyear(time: number, timezone?: string): number; tradingDay(time: number): number; now(bars?: Bars): number; }; session: { isMarket(bars: Bars): boolean; isFirstBar(bars: Bars): boolean; isLastBar(bars: Bars): boolean; }; tf: { period(bars: Bars): string; multiplier(bars: Bars): number; /** An interval input's value as a Pine timeframe string; `'chart'`, and with `chart` given its interval's code, is the chart's (`''`). */ fromInterval(code: string, chart?: Bars): string; /** A Pine timeframe as the interval a request opens; `undefined` is the chart's. */ toInterval(timeframe: string, chart: Bars): string | undefined; inSeconds(timeframe: string): number; fromSeconds(seconds: number): string; isIntraday(bars: Bars): boolean; isDaily(bars: Bars): boolean; isWeekly(bars: Bars): boolean; isMonthly(bars: Bars): boolean; isDwm(bars: Bars): boolean; isMinutes(bars: Bars): boolean; isSeconds(bars: Bars): boolean; isTicks(bars: Bars): boolean; }; syminfo: { tickerid(bars: Bars): string; prefix(bars: Bars): string; root(bars: Bars): string; type(bars: Bars): string; session(bars: Bars): string; pricescale(bars: Bars): number; mincontract(bars: Bars): number; prefixOf(symbol: string): string; tickerOf(symbol: string): string; }; ticker: { make(prefix: string, ticker: string, ...rest: unknown[]): string; modify(tickerid: string, ...rest: unknown[]): string; standard(symbol?: string): string; inherit(from: string, symbol: string): string; }; /** `x[n]` with a computed `n`: na when `n` is na, negative, fractional or past the 5,000 bars kept. */ agoOf(series: Series, n: number): number; /** `close[n]` with a computed `n`: the bar `n` back, or a bar of na values. */ barAgo(bars: Bars, n: number): BarView; /** `bar_index[n]`. */ barsAgoIndex(bars: Bars, n: number): number; isLastConfirmedHistory(bars: Bars): boolean; // --- drawings ------------------------------------------------------------------------------ line: { 'new'(options: PineLineOptions): PineLine; setX1(l: PineLine, x: number): void; setX2(l: PineLine, x: number): void; setY1(l: PineLine, y: number): void; setY2(l: PineLine, y: number): void; setXy1(l: PineLine, x: number, y: number): void; setXy2(l: PineLine, x: number, y: number): void; setXloc(l: PineLine, x1: number, x2: number, xloc: string): void; setColor(l: PineLine, color: Color | null): void; setExtend(l: PineLine, extend: string): void; setStyle(l: PineLine, style: string): void; setWidth(l: PineLine, width: number): void; setFirstPoint(l: PineLine, point: PinePoint): void; setSecondPoint(l: PineLine, point: PinePoint): void; delete(l: PineLine): void; getX1(l: PineLine): number; getX2(l: PineLine): number; getY1(l: PineLine): number; getY2(l: PineLine): number; getPrice(l: PineLine, x: number): number; copy(l: PineLine): PineLine; all(): PineArray; }; label: { 'new'(options: PineLabelOptions): PineLabel; setX(l: PineLabel, x: number): void; setY(l: PineLabel, y: number): void; setXy(l: PineLabel, x: number, y: number): void; setXloc(l: PineLabel, x: number, xloc: string): void; setYloc(l: PineLabel, yloc: string): void; setText(l: PineLabel, text: string): void; setTextcolor(l: PineLabel, color: Color | null): void; setColor(l: PineLabel, color: Color | null): void; setStyle(l: PineLabel, style: string): void; setSize(l: PineLabel, size: string | number): void; setTextalign(l: PineLabel, align: string): void; setTooltip(l: PineLabel, tooltip: string): void; setPoint(l: PineLabel, point: PinePoint): void; setTextFontFamily(l: PineLabel, family: string): void; setTextFormatting(l: PineLabel, formatting: string): void; delete(l: PineLabel): void; getX(l: PineLabel): number; getY(l: PineLabel): number; getText(l: PineLabel): string; copy(l: PineLabel): PineLabel; all(): PineArray; }; box: { 'new'(options: PineBoxOptions): PineBox; setLeft(b: PineBox, left: number): void; setRight(b: PineBox, right: number): void; setTop(b: PineBox, top: number): void; setBottom(b: PineBox, bottom: number): void; setLefttop(b: PineBox, left: number, top: number): void; setRightbottom(b: PineBox, right: number, bottom: number): void; setTopLeftPoint(b: PineBox, point: PinePoint): void; setBottomRightPoint(b: PineBox, point: PinePoint): void; setXloc(b: PineBox, left: number, right: number, xloc: string): void; setBorderColor(b: PineBox, color: Color | null): void; setBorderWidth(b: PineBox, width: number): void; setBorderStyle(b: PineBox, style: string): void; setBgcolor(b: PineBox, color: Color | null): void; setExtend(b: PineBox, extend: string): void; setText(b: PineBox, text: string): void; setTextColor(b: PineBox, color: Color | null): void; setTextSize(b: PineBox, size: string | number): void; setTextHalign(b: PineBox, align: string): void; setTextValign(b: PineBox, align: string): void; setTextWrap(b: PineBox, wrap: string): void; setTextFontFamily(b: PineBox, family: string): void; setTextFormatting(b: PineBox, formatting: string): void; delete(b: PineBox): void; getLeft(b: PineBox): number; getRight(b: PineBox): number; getTop(b: PineBox): number; getBottom(b: PineBox): number; copy(b: PineBox): PineBox; all(): PineArray; }; table: { 'new'(options: PineTableOptions): PineTable; cell(options: PineCellOptions): void; cellSetText(t: PineTable, column: number, row: number, text: string): void; cellSetBgcolor(t: PineTable, column: number, row: number, color: Color | null): void; cellSetTextColor(t: PineTable, column: number, row: number, color: Color | null): void; cellSetTextSize(t: PineTable, column: number, row: number, size: string | number): void; cellSetTextHalign(t: PineTable, column: number, row: number, align: string): void; cellSetTextValign(t: PineTable, column: number, row: number, align: string): void; cellSetWidth(t: PineTable, column: number, row: number, width: number): void; cellSetHeight(t: PineTable, column: number, row: number, height: number): void; cellSetTooltip(t: PineTable, column: number, row: number, tooltip: string): void; cellSetTextFontFamily(t: PineTable, column: number, row: number, family: string): void; cellSetTextFormatting(t: PineTable, column: number, row: number, formatting: string): void; clear(t: PineTable, startColumn?: number, startRow?: number, endColumn?: number, endRow?: number): void; mergeCells(t: PineTable, startColumn: number, startRow: number, endColumn: number, endRow: number): void; delete(t: PineTable): void; setPosition(t: PineTable, position: string): void; setBgcolor(t: PineTable, color: Color | null): void; setBorderColor(t: PineTable, color: Color | null): void; setBorderWidth(t: PineTable, width: number): void; setFrameColor(t: PineTable, color: Color | null): void; setFrameWidth(t: PineTable, width: number): void; all(): PineArray; }; polyline: { 'new'(options: PinePolylineOptions): PinePolyline; delete(p: PinePolyline): void; all(): PineArray; }; /** Pine's linefills, on `draw.linefill`. */ linefill: { 'new'(line1: PineLine, line2: PineLine, color: Color | null): PineLinefill; setColor(f: PineLinefill, color: Color | null): void; getLine1(f: PineLinefill): PineLine; getLine2(f: PineLinefill): PineLine; delete(f: PineLinefill): void; all(): PineArray; }; /** Volume footprints from our trades: taker buying and selling per row, TradingView's rules for rows, value area and imbalances. */ footprint: { /** One `request.footprint` call site on the bars of `bars`, made at setup. */ source(bars: Bars, ticksPerRow: number, vaPercent?: number, imbalancePercent?: number): { get(): PineFootprint }; buyVolume(f: PineFootprint): number; sellVolume(f: PineFootprint): number; totalVolume(f: PineFootprint): number; delta(f: PineFootprint): number; poc(f: PineFootprint): PineVolumeRow; vah(f: PineFootprint): PineVolumeRow; val(f: PineFootprint): PineVolumeRow; rows(f: PineFootprint): PineArray; rowByPrice(f: PineFootprint, price: number): PineVolumeRow; }; volumeRow: { upPrice(r: PineVolumeRow): number; downPrice(r: PineVolumeRow): number; buyVolume(r: PineVolumeRow): number; sellVolume(r: PineVolumeRow): number; totalVolume(r: PineVolumeRow): number; delta(r: PineVolumeRow): number; hasBuyImbalance(r: PineVolumeRow): boolean; hasSellImbalance(r: PineVolumeRow): boolean; }; /** `request.security_lower_tf`: made at setup with the bars it is read on and the lower bars. */ intrabars(bars: Bars, lower: Bars, ignoreInvalid?: boolean): PineIntrabars; point: { make(time: number, index: number, price: number): PinePoint; fromIndex(index: number, price: number): PinePoint; fromTime(time: number, price: number): PinePoint; now(bars: Bars, price: number): PinePoint; copy(p: PinePoint): PinePoint; }; // --- small things -------------------------------------------------------------------------- /** Pine v5's `and` and `or`, which evaluate both sides. */ and(a: boolean, b: boolean): boolean; or(a: boolean, b: boolean): boolean; /** A `for` loop's step: negative when the start is above the end. */ step(from: number, to: number, by: number): number; inRange(i: number, to: number, step: number): boolean; /** A number as a condition: true when not na and not 0. */ truthy(value: unknown): boolean; /** `nz()` of a string, colour or object. */ nzRef(value: T, replacement?: T): T; /** An alert message with Pine's `{{close}}`-style placeholders filled in. */ alertMessage(template: string, bars: Bars): string; plotArrow(markers: MarkersPlot, value: number, colorUp?: Color | null, colorDown?: Color | null): void; /** `runtime.error(message)`: stops the script with the message. */ error(message: string): never; }; ``` ## Examples ### examples/opening-closing-flow.ts: Opening and closing flow Split taker flow into trades that opened positions and trades that closed them, from each trade's own change in open interest. ```ts // Taker flow split by what each trade did to open interest. A buy that raised open interest opened // a long; a buy that lowered it closed a short. Opening flow is new longs less new shorts, closing // flow is shorts bought back less longs sold out, in USD per bar: opening as the bars, closing as // the line. From each trade's own change in open interest (`oiDelta`), which 0xArchive records on // every print. indicator('Opening and closing flow', { shortName: 'Open/close flow' }); const minUsd = input.float('Smallest trade ($)', 0, { min: 0 }); const chart = bars(); const tape = trades({ minNotional: minUsd }); const opening = tally(); const closing = tally(); const openingBars = plot.histogram('Opening flow $'); const closingLine = plot.line('Closing flow $', { color: color.study(12), width: 1 }); tape.onTrade((t) => { // 0: one side opened and the other closed, so open interest did not move. na: the venue did not say. if (isNa(t.oiDelta) || t.oiDelta === 0) return; const usd = t.side === 'buy' ? t.notional : -t.notional; (t.oiDelta > 0 ? opening : closing).add(usd); }); chart.onBar(() => { // Trades reach back to the start of the UTC day before yesterday; older bars stay empty. if (!tape.available) return; openingBars.set(opening.value, color.withAlpha(opening.value >= 0 ? color.up : color.down, 0.75)); closingLine.set(closing.value); }); ``` ### examples/squeeze-or-new-positions.ts: Squeeze or new positions Label every big candle by whether it was driven by positions closing (a squeeze or a flush) or by new positions opening. ```ts // Big candles labelled by what drove them: a rally that bought back shorts (a squeeze) or one that // opened longs, a drop that sold out longs (a flush) or one that opened shorts. A candle is big when // its body is more than `size` times the 14-bar ATR. Each trade is sorted by its change in open // interest (`oiDelta`), as Opening and closing flow does. indicator('Squeeze or new positions', { overlay: true, shortName: 'Squeeze' }); const size = input.float('Body over ATR', 1.5, { min: 0.1, step: 0.1 }); const chart = bars(); const tape = trades(); const atr = ta.atr(14); const opening = tally(); const closing = tally(); const marks = plot.markers('Drivers'); tape.onTrade((t) => { if (isNa(t.oiDelta) || t.oiDelta === 0) return; (t.oiDelta > 0 ? opening : closing).add(t.side === 'buy' ? t.notional : -t.notional); }); chart.onClose(() => { // Updated on every bar, before any return: an indicator only advances when `update` runs. const range = atr.update(chart); const body = chart.close - chart.open; if (!tape.available || !(Math.abs(body) > size * range)) return; const up = body > 0; // In the direction of the candle: positions closed against positions opened. const exits = up ? closing.value : -closing.value; const entries = up ? opening.value : -opening.value; const squeeze = exits > entries; const text = up ? (squeeze ? 'Squeeze' : 'New longs') : (squeeze ? 'Flush' : 'New shorts'); const ink = up ? color.up : color.down; marks.add({ position: up ? 'above' : 'below', shape: squeeze ? 'diamond' : 'circle', text, color: squeeze ? ink : color.withAlpha(ink, 0.6) }); }); ``` ### examples/liquidated-wallets.ts: Most-liquidated wallets Rank the wallets liquidated for the most this week, and mark the biggest one on each bar with its address. ```ts // The wallets liquidated for the most over the last 7 days, in a table, and the biggest wallet // liquidated on a bar marked with its address. Hyperliquid names the liquidated wallet on every // liquidation; Lighter names an account index, shown as it is. One position is often liquidated in // several fills a moment apart, so fills of one wallet within a minute count as one liquidation. indicator('Most-liquidated wallets', { overlay: true, shortName: 'Liquidated wallets' }); const rows = input.int('Wallets', 8, { min: 1, max: 20 }); const markUsd = input.float('Mark a wallet from ($)', 1_000_000, { min: 0 }); const position = input.choice('Position', 'top-right', ['top-left', 'top-right', 'bottom-left', 'bottom-right'], { legend: false }); const chart = bars(); const liq = liquidations(); // Per wallet, across every bar. An event handler may change only tallies and live state. const usdBy = state.map(50_000, 'live'); const timesBy = state.map(50_000, 'live'); const longUsdBy = state.map(50_000, 'live'); const lastSeen = state.map(50_000, 'live'); // This bar's liquidated USD per wallet, for the marker. const thisBar = tallyBy(2_000); const marks = plot.markers('Big wallet liquidations'); const table = draw.table({ position, columns: 4, rows: 21, background: color.withAlpha(color.panel, 0.92), border: color.withAlpha(color.neutral, 0.12), frame: color.withAlpha(color.neutral, 0.28) }); const MINUTE = 60_000; const short = (wallet: string) => (wallet.length > 12 ? `${wallet.slice(0, 6)}…${wallet.slice(-4)}` : wallet); liq.onLiquidation((l) => { if (l.user === null) return; const wallet = l.user.toLowerCase(); const last = lastSeen.get(wallet); if (last === undefined || l.time - last > MINUTE) timesBy.set(wallet, (timesBy.get(wallet) ?? 0) + 1); lastSeen.set(wallet, l.time); usdBy.set(wallet, (usdBy.get(wallet) ?? 0) + l.notional); if (l.side === 'long') longUsdBy.set(wallet, (longUsdBy.get(wallet) ?? 0) + l.notional); thisBar.add(wallet, l.side === 'long' ? l.notional : -l.notional); }); chart.onBar(() => { let top = ''; let topUsd = 0; for (const [wallet, usd] of thisBar.entries()) { if (Math.abs(usd) > Math.abs(topUsd)) { top = wallet; topUsd = usd; } } // `available` is false on bars older than the history, and on the newest while it can still change. if (liq.available && Math.abs(topUsd) >= markUsd) { // Longs are liquidated on the way down, so their mark sits under the candle. const long = topUsd > 0; marks.add({ position: long ? 'below' : 'above', shape: 'circle', text: `${short(top)} ${fmt.usd(Math.abs(topUsd))}`, color: long ? color.down : color.up }); } if (!chart.isLast) return; const ranked = [...usdBy.entries()].sort((a, b) => b[1] - a[1]).slice(0, rows); const dim = color.withAlpha(color.neutral, 0.7); table.clear(); table.cell(0, 0, 'Wallet', { align: 'left', size: 'small', color: dim }); table.cell(1, 0, 'Liquidated', { align: 'right', size: 'small', color: dim }); table.cell(2, 0, 'Times', { align: 'right', size: 'small', color: dim }); table.cell(3, 0, 'Side', { align: 'left', size: 'small', color: dim }); ranked.forEach(([wallet, usd], i) => { const longShare = (longUsdBy.get(wallet) ?? 0) / usd; const side = longShare > 0.5 ? 'Long' : 'Short'; table.cell(0, i + 1, short(wallet), { align: 'left', tooltip: wallet }); table.cell(1, i + 1, fmt.usd(usd), { align: 'right' }); table.cell(2, i + 1, String(timesBy.get(wallet) ?? 0), { align: 'right' }); table.cell(3, i + 1, side, { align: 'left', color: side === 'Long' ? color.down : color.up }); }); }); ``` ### examples/liquidation-levels-ahead.ts: Liquidation levels ahead Draw the biggest liquidation clusters above and below the price, and the balance of shorts above against longs below on every bar. ```ts // Where the price would set off liquidations next, from 0xArchive's liquidation levels: the three // biggest clusters of short positions above the price and of long positions below it, within // `within` percent, as lines out to the right labelled with their size. Below the candles, the // balance within the same distance on each bar: shorts above less longs below, from the snapshot // as of that bar. indicator('Liquidation levels ahead', { overlay: true, shortName: 'Liq levels', maxLines: 6, maxLabels: 6 }); const within = input.float('Within (%)', 4, { min: 0.5, max: 10, step: 0.5 }); const chart = bars(); const map = liquidationLevels(); const balance = plot.histogram('Shorts above less longs below $', { pane: 'balance' }); const SHOWN = 3; // How far back the lines start, in bars; their labels sit there, clear of the price axis. const BACK = 48; chart.onBar(() => { if (!map.available) return; const price = chart.close; const near = map.levels().filter((level) => Math.abs(level.price / price - 1) * 100 <= within); let shortsAbove = 0; let longsBelow = 0; for (const level of near) { if (level.price > price) shortsAbove += level.shortUsd; else longsBelow += level.longUsd; } balance.set(shortsAbove - longsBelow, color.withAlpha(shortsAbove >= longsBelow ? color.up : color.down, 0.7)); if (!chart.isLast) return; // Biggest first: shorts above the price, longs below. const above = near.filter((level) => level.price > price && level.shortUsd > 0).sort((a, b) => b.shortUsd - a.shortUsd).slice(0, SHOWN); const below = near.filter((level) => level.price < price && level.longUsd > 0).sort((a, b) => b.longUsd - a.longUsd).slice(0, SHOWN); above.forEach((level, rank) => mark(level.price, level.shortUsd, 'shorts', color.up, rank)); below.forEach((level, rank) => mark(level.price, level.longUsd, 'longs', color.down, rank)); }); function mark(price: number, usd: number, side: string, ink: string, rank: number) { draw.line({ from: { bar: chart.index - BACK, price }, to: { bar: chart.index, price }, extend: 'right', color: color.withAlpha(ink, rank === 0 ? 0.9 : 0.6), width: rank === 0 ? 2 : 1, style: rank === 0 ? 'solid' : 'dashed' }); // The label's lower-left corner on the line's start: it sits just above the line. draw.label({ at: { bar: chart.index - BACK, price }, text: `${fmt.usd(usd)} ${side}`, style: 'lowerLeft', color: color.none, textColor: ink, size: 'small' }); } ``` ### examples/large-takers.ts: Large taker orders Mark every bar where a single taker order filled for more than a size, joined from its fills. ```ts // Taker orders above a size, joined from their fills. One mark per bar and side: the orders' total, and how many when more than one. indicator('Large taker orders', { overlay: true }); const minUsd = input.float('Minimum order ($)', 1_000_000, { min: 0 }); const gap = input.int('Bars between labels', 6, { min: 1, max: 200 }); const chart = bars(); const tape = trades(); const orders = tallyBy(512); const marks = plot.markers('Large takers'); // The bar of each side's last label: a mark closer than `gap` bars keeps its dot and drops its text, so labels never print over each other. const lastBuyLabel = state.bar(-gap); const lastSellLabel = state.bar(-gap); // The fills of one taker order come one after another, and each after the first says so: a new order starts where `sameOrder` is false. const order = state.live(0); tape.onTrade((t) => { if (!t.sameOrder) order.value += 1; orders.add(String(order.value), t.side === 'buy' ? t.notional : -t.notional); }); chart.onClose(() => { let buyUsd = 0; let buys = 0; let sellUsd = 0; let sells = 0; for (const [, usd] of orders.entries()) { if (usd >= minUsd) { buyUsd += usd; buys += 1; } if (-usd >= minUsd) { sellUsd -= usd; sells += 1; } } if (buys > 0) { const labelled = chart.index - lastBuyLabel.value >= gap; if (labelled) lastBuyLabel.value = chart.index; const text = buys > 1 ? `${fmt.usd(buyUsd)} ×${buys}` : fmt.usd(buyUsd); marks.add({ position: 'below', shape: 'circle', text: labelled ? text : '', color: color.up }); } if (sells > 0) { const labelled = chart.index - lastSellLabel.value >= gap; if (labelled) lastSellLabel.value = chart.index; const text = sells > 1 ? `${fmt.usd(sellUsd)} ×${sells}` : fmt.usd(sellUsd); marks.add({ position: 'above', shape: 'circle', text: labelled ? text : '', color: color.down }); } }); ``` ### examples/liquidations.ts: Liquidations Liquidated USD per bar, longs below zero and shorts above, optionally for chosen wallets only. ```ts // Liquidated USD per bar: longs below zero, shorts above. Optionally only some wallets' (Hyperliquid names the liquidated wallet). indicator('Liquidations', { precision: 0 }); const minUsd = input.float('Smallest ($)', 0, { min: 0 }); const wallets = input.addresses('Only these wallets', []); const chart = bars(); const liq = liquidations({ minNotional: minUsd }); const longs = tally(); const shorts = tally(); const longBars = plot.histogram('Longs liquidated $', { precision: 0 }); const shortBars = plot.histogram('Shorts liquidated $', { precision: 0 }); liq.onLiquidation((l) => { // On Lighter the liquidated account is an index, which no wallet matches. if (wallets.length > 0 && !(l.user !== null && wallets.includes(l.user.toLowerCase()))) return; (l.side === 'long' ? longs : shorts).add(l.price * l.size); }); chart.onBar(() => { if (!liq.available) return; longBars.set(-longs.value, color.down); shortBars.set(shorts.value, color.up); }); ``` ### examples/liquidation-clusters.ts: Liquidation clusters The single biggest long and short liquidation level within 10% of the price, as of each bar. ```ts // The biggest cluster of long and short liquidations within 10% of the price, from the snapshot as of each bar. // Clusters sit up to 10% away, so they leave the price scale to the candles (autoscale: false). indicator('Liquidation clusters', { overlay: true }); const chart = bars(); const map = liquidationLevels(); const longs = plot.points('Long cluster', { color: color.down, autoscale: false }); const shorts = plot.points('Short cluster', { color: color.up, autoscale: false }); chart.onBar(() => { let bestLong = 0; let bestShort = 0; let longAt = na; let shortAt = na; for (const level of map.levels()) { if (level.longUsd > bestLong) { bestLong = level.longUsd; longAt = level.price; } if (level.shortUsd > bestShort) { bestShort = level.shortUsd; shortAt = level.price; } } longs.set(longAt); shorts.set(shortAt); }); ``` ### examples/cvd.ts: Tape CVD Cumulative volume delta from the tape, in USD, with each bar's delta as a histogram. ```ts // Cumulative volume delta from the tape, in USD like the chart's own CVD: each bar's taker buys minus sells (size times price), summed. indicator('Tape CVD'); const chart = bars(); const tape = trades(); const delta = tally(); const total = state.bar(0); const cvd = plot.line('CVD'); const deltaBars = plot.histogram('Delta'); tape.onTrade((t) => delta.add(t.side === 'buy' ? t.notional : -t.notional)); chart.onBar(() => { if (!tape.available) return; deltaBars.set(delta.value, delta.value >= 0 ? color.up : color.down); cvd.set(total.value + delta.value); }); chart.onClose(() => { total.value += delta.value; }); ``` ### examples/funding-oi.ts: Funding and open interest Funding annualised from the hourly rate, and open interest in USD, from 5-minute samples. ```ts // Funding, annualised from the hourly rate, and open interest in USD, as of each bar: the last 5-minute sample before it closed. indicator('Funding and open interest'); const chart = bars(); const perp = derivatives(); const funding = plot.histogram('Funding % a year', { precision: 2 }); // A pane of its own: funding (a few percent) and open interest (billions) share no scale. const oi = plot.line('Open interest $', { pane: 'oi' }); chart.onBar(() => { if (!perp.available) return; funding.set(perp.funding * 24 * 365 * 100, perp.funding >= 0 ? color.up : color.down); oi.set(perp.openInterestUsd); }); ``` ### examples/breadth-above-vwap.ts: Breadth above VWAP The share of Hyperliquid's perps trading above their session VWAP, as of each bar. ```ts // The share of Hyperliquid's perps above their session VWAP, as of each bar. indicator('Breadth above VWAP'); const chart = bars(); const perps = breadth(); const line = plot.line('Above VWAP %', { range: { min: 0, max: 100 }, precision: 1 }); plot.level('Half', 50, { style: 'dashed' }); chart.onBar(() => { if (!perps.available) return; line.set(perps.value); }); ``` ### examples/book-imbalance.ts: Bid share within a band The share of resting size on the bid within a band around the mid, from the live book. ```ts // The bid's share of resting size within a band around the mid. indicator('Bid share within a band'); // The book the page shows reaches about 0.06% on BTC at its default 20 levels; show more levels for a wider band. const band = input.float('Band (%)', 0.05, { min: 0.01, max: 10, step: 0.01 }); const chart = bars(); const book = depth({ bandsPct: [band] }); const share = plot.line('Bid share', { range: { min: 0, max: 1 } }); plot.level('Even', 0.5, { style: 'dashed' }); chart.onBar(() => { if (!book.available) return; const bid = book.bid(band); const ask = book.ask(band); share.set(bid + ask > 0 ? bid / (bid + ask) : na); }); ``` ### examples/ema.ts: EMA An exponential moving average, seeded the way TradingView seeds it. ```ts // The built-in EMA, as a script. The engine's ta.ema follows TradingView: it is seeded with the // simple average of the first `length` values, so the first bars differ from the chart's old EMA. indicator('EMA', { overlay: true }); const length = input.int('Length', 20, { min: 1 }); const source = input.source('Source', 'close'); const chart = bars(); const ema = ta.ema(length); const line = plot.line('EMA'); chart.onBar(() => { line.set(ema.update(chart.source(source))); }); ``` ### examples/higher-timeframe.ts: 4h EMA An EMA of 4-hour bars on any chart, with no lookahead. ```ts // A 4h EMA on any chart. Reading another stream gives its last closed value: no lookahead. indicator('4h EMA', { overlay: true }); const length = input.int('Length', 20, { min: 1 }); const chart = bars(); const h4 = bars({ interval: '4h' }); const ema = ta.ema(length); const line = plot.line('4h EMA', { width: 2 }); h4.onBar(() => { ema.update(h4.close); }); chart.onBar(() => { line.set(ema.confirmed); }); ``` ### examples/anchored-vwap.ts: Anchored VWAP bands VWAP and deviation bands from a moment picked on the chart. ```ts // VWAP from a moment picked on the chart: in the settings, press "Pick on chart" and click the // bar to start from. Until one is picked it starts at the first loaded bar. The bands sit a // chosen number of volume-weighted standard deviations either side, as TradingView's anchored // VWAP draws them, and a label marks the anchor. indicator('Anchored VWAP bands', { shortName: 'AVWAP bands', overlay: true }); const anchor = input.time('Anchor', null); const source = input.source('Source', 'hlc3'); const width = input.float('Bands (deviations)', 1, { min: 0.5, max: 3, step: 0.5 }); const note = input.string('Anchor label', 'Anchor'); const chart = bars(); const volume = state.bar(0); const weighted = state.bar(0); const squares = state.bar(0); const tone = color.study(3); const vwap = plot.line('VWAP', { width: 2, color: tone }); const upper = plot.line('Upper band', { color: color.withAlpha(tone, 0.55) }); const lower = plot.line('Lower band', { color: color.withAlpha(tone, 0.55) }); const band = plot.fill('Band', upper, lower, { color: color.withAlpha(tone, 0.07) }); const start = plot.shape('Anchor', { shape: 'labelUp', location: 'below', color: tone, text: note }); chart.onBar(() => { if (anchor !== null && chart.time < anchor) return; const first = volume.value === 0; const price = chart.source(source); // A bar with no volume yet (a minute with no trades, or one just opened) adds nothing and keeps the VWAP. const traded = isNa(price) ? 0 : nz(chart.volume); if (traded > 0) { volume.value += traded; weighted.value += price * traded; squares.value += price * price * traded; } if (volume.value === 0) return; const mean = weighted.value / volume.value; const deviation = Math.sqrt(Math.max(0, squares.value / volume.value - mean * mean)); vwap.set(mean); upper.set(mean + width * deviation); lower.set(mean - width * deviation); start.set(first); }); ``` ### examples/open-bands.ts: Open bands Bands a set percentage above and below the day's open, kept out of the price scale. ```ts // Bands a set percentage above and below the day's open (00:00 UTC), for how far the day has // stretched. Bands far from price are drawn where they fall, but unless "Fit bands in view" is // on they do not stretch the price scale (autoscale: false), so the candles keep the view. indicator('Open bands', { overlay: true }); const near = input.float('Near band (%)', 2, { min: 0.1, max: 50, step: 0.1 }); const far = input.float('Far band (%)', 5, { min: 0.1, max: 50, step: 0.1 }); const fit = input.bool('Fit bands in view', false); const chart = bars(); const day = state.bar(na); const open = state.bar(na); const tone = color.study(12); const farAbove = plot.line('Far above', { color: color.withAlpha(tone, 0.55), style: 'dashed', autoscale: fit }); const nearAbove = plot.line('Near above', { color: tone, autoscale: fit }); const dayOpen = plot.line('Open', { color: color.withAlpha(color.neutral, 0.6), style: 'dotted', autoscale: fit }); const nearBelow = plot.line('Near below', { color: tone, autoscale: fit }); const farBelow = plot.line('Far below', { color: color.withAlpha(tone, 0.55), style: 'dashed', autoscale: fit }); chart.onBar(() => { const start = clock.startOf(chart.time, 'day'); if (start !== day.value) { day.value = start; open.value = chart.open; } const base = open.value; farAbove.set(base * (1 + far / 100)); nearAbove.set(base * (1 + near / 100)); dayOpen.set(base); nearBelow.set(base * (1 - near / 100)); farBelow.set(base * (1 - far / 100)); }); ``` ### examples/relative-strength.ts: Relative strength The chart's market against another, both rebased to 100. ```ts // The chart's market against another, both rebased to 100 at the first bar they share: above // 100 the chart's market has done better since then. Against ETH by default, so BTC and HYPE, // the charts most often open, read against something else; on an ETH chart, or with "Chart" in // the market setting, it compares the market with itself, a flat line at 100. indicator('Relative strength', { shortName: 'RS', precision: 2 }); const against = input.market('Against', 'ETH'); const chart = bars(); const other = bars(against === null ? {} : { market: against }); const base = state.bar(na); const ratio = plot.line('Relative strength', { width: 2 }); const par = plot.level('Par', 100, { style: 'dashed' }); chart.onBar(() => { if (isNa(other.close) || other.close === 0) return; const value = chart.close / other.close; if (isNa(base.value)) base.value = value; ratio.set((100 * value) / base.value); }); ``` ### examples/trend-bars.ts: Trend bars Candles coloured by trend, with a label where the close flips across the EMA. ```ts // Candles coloured by trend, as Pine's barcolor() colours them: the up colour while the close // holds above a rising EMA, the down colour below a falling one, grey in between. A label marks // a flip of the close across the EMA that holds for two bars, drawn on the first of them, and // no sooner than a set number of bars after the last label, so chop does not stack them. A dot // marks a bar twice as wide as the recent average. indicator('Trend bars', { overlay: true }); const length = input.int('EMA length', 34, { min: 2 }); // How labels are spaced is not what the script computes, so it stays out of the legend name. const gap = input.int('Bars between labels', 10, { min: 1, legend: false }); const chart = bars(); const ema = ta.ema(length); const rising = ta.rising(1); const falling = ta.falling(1); const range = ta.sma(20); // Which side of the EMA the close has held, the side it was on one bar ago, and bars since a label. const held = state.bar(0); const before = state.bar(0); const quiet = state.bar(1e9); const line = plot.line('EMA', { width: 2 }); const trend = plot.barColor('Trend'); // Set on the second bar of a flip, drawn one bar back on its first. const up = plot.shape('Flips up', { shape: 'labelUp', location: 'below', color: color.up, text: 'Up', offset: -1 }); const down = plot.shape('Flips down', { shape: 'labelDown', location: 'above', color: color.down, text: 'Down', offset: -1 }); const wide = plot.shape('Wide bar', { char: '•', location: 'above', color: color.neutral, size: 'normal' }); chart.onBar(() => { const e = ema.update(chart.close); const isRising = rising.update(e); const isFalling = falling.update(e); const average = range.update(chart.high - chart.low); line.set(e); if (isNa(e)) trend.set(null); else if (chart.close > e && isRising) trend.set(color.up); else if (chart.close < e && isFalling) trend.set(color.down); else trend.set(color.withAlpha(color.neutral, 0.55)); const side = isNa(e) ? 0 : chart.close > e ? 1 : chart.close < e ? -1 : 0; const flipped = side !== 0 && side === before.value && side !== held.value; const labelled = flipped && held.value !== 0 && quiet.value >= gap; up.set(labelled && side > 0); down.set(labelled && side < 0); if (flipped) held.value = side; quiet.value = labelled ? 1 : quiet.value + 1; before.value = side; wide.set(chart.high - chart.low > 2 * average); }); ``` ### examples/session-table.ts: Session table The last price, change, high and low since 00:00 UTC, in a table in a corner of the chart. ```ts // The session so far, in a table in a corner of the chart: the last price, the change since the // session opened at 00:00 UTC, and the session's high and low. The title is an input, shown as // text only, and the corner is a choice of Pine's nine positions. indicator('Session table', { overlay: true }); const title = input.string('Title', 'Session'); // Where the table sits is not what it computes, so it stays out of the name in the legend. const position = input.choice('Position', 'top-right', ['top-left', 'top-center', 'top-right', 'middle-left', 'middle-center', 'middle-right', 'bottom-left', 'bottom-center', 'bottom-right'], { legend: false }); const chart = bars(); const day = state.bar(na); const open = state.bar(na); const high = state.bar(na); const low = state.bar(na); const table = draw.table({ position, columns: 2, rows: 5, background: color.withAlpha(color.panel, 0.92), border: color.withAlpha(color.neutral, 0.14), frame: color.withAlpha(color.neutral, 0.28) }); chart.onBar(() => { const start = clock.startOf(chart.time, 'day'); if (start !== day.value) { day.value = start; open.value = chart.open; high.value = chart.high; low.value = chart.low; } high.value = Math.max(high.value, chart.high); low.value = Math.min(low.value, chart.low); if (!chart.isLast) return; const change = chart.close / open.value - 1; // The title spans both columns. table.merge(0, 0, 1, 0); table.cell(0, 0, title, { align: 'left', size: 'small', color: color.withAlpha(color.neutral, 0.8) }); table.cell(0, 1, 'Last', { align: 'left', color: color.withAlpha(color.neutral, 0.8) }); table.cell(1, 1, fmt.price(chart.close), { align: 'right' }); table.cell(0, 2, 'Change', { align: 'left', color: color.withAlpha(color.neutral, 0.8), tooltip: 'Since the session opened at 00:00 UTC' }); table.cell(1, 2, `${change >= 0 ? '+' : ''}${fmt.pct(change)}`, { align: 'right', color: change >= 0 ? color.up : color.down }); table.cell(0, 3, 'High', { align: 'left', color: color.withAlpha(color.neutral, 0.8) }); table.cell(1, 3, fmt.price(high.value), { align: 'right' }); table.cell(0, 4, 'Low', { align: 'left', color: color.withAlpha(color.neutral, 0.8) }); table.cell(1, 4, fmt.price(low.value), { align: 'right' }); }); ``` ### examples/ichimoku.ts: Ichimoku Ichimoku Kinko Hyo, with the leading spans and cloud drawn ahead of the newest bar. ```ts // Ichimoku Kinko Hyo, drawn as TradingView's built-in draws it: the two leading spans, and the // cloud between them, shifted 25 bars ahead, into the space past the newest bar; the lagging // span, the close, shifted 25 bars back. Each line is the midpoint of the highest high and the // lowest low over its window. indicator('Ichimoku', { overlay: true }); const conversionLength = input.int('Conversion line length', 9, { min: 1 }); const baseLength = input.int('Base line length', 26, { min: 1 }); const spanBLength = input.int('Leading span B length', 52, { min: 1 }); const displacement = input.int('Displacement', 26, { min: 1 }); const chart = bars(); const conversionHigh = ta.highest(conversionLength); const conversionLow = ta.lowest(conversionLength); const baseHigh = ta.highest(baseLength); const baseLow = ta.lowest(baseLength); const spanBHigh = ta.highest(spanBLength); const spanBLow = ta.lowest(spanBLength); // Blue, red and green as on TradingView, from the theme's study colours. const conversion = plot.line('Conversion line', { color: color.study(12) }); const base = plot.line('Base line', { color: color.study(6) }); // TradingView counts the displacement from the current bar, so the shift is one bar less. const lagging = plot.line('Lagging span', { color: color.study(10), offset: 1 - displacement }); const spanA = plot.line('Leading span A', { color: color.withAlpha(color.up, 0.75), offset: displacement - 1 }); const spanB = plot.line('Leading span B', { color: color.withAlpha(color.down, 0.75), offset: displacement - 1 }); const cloud = plot.fill('Cloud', spanA, spanB); chart.onBar(() => { const c = (conversionHigh.update(chart.high) + conversionLow.update(chart.low)) / 2; const b = (baseHigh.update(chart.high) + baseLow.update(chart.low)) / 2; const a = (c + b) / 2; const s = (spanBHigh.update(chart.high) + spanBLow.update(chart.low)) / 2; conversion.set(c); base.set(b); lagging.set(chart.close); spanA.set(a); spanB.set(s); cloud.set(a > s ? color.withAlpha(color.up, 0.12) : color.withAlpha(color.down, 0.12)); }); ``` ### examples/rsi-cross.ts: RSI cross An RSI with a mark where it crosses up through 30, written the way the Pine translation writes it. ```ts // What the Pine front end would emit for a small RSI script (docs/scripting.md, "Pine"). // // //@version=5 // indicator("RSI cross") // len = input.int(14, "Length") // r = ta.rsi(close, len) // var int crosses = 0 // if ta.crossover(r, 30) // crosses += 1 // plot(r, "RSI") // plotshape(ta.crossover(r, 30), "Up", shape.triangleup, location.bottom) // lower = hline(30, "Oversold") // upper = hline(70, "Overbought") // fill(upper, lower, color=color.new(color.purple, 90)) indicator('RSI cross'); const len = input.int('Length', 14); const chart = bars(); const rsi = ta.rsi(len); const crossInIf = ta.crossover(); const crossInPlot = ta.crossover(); const crosses = state.bar(0); const history = series(2); const p0 = plot.line('RSI'); const s0 = plot.shape('Up', { shape: 'triangleUp', location: 'bottom' }); const lower = plot.level('Oversold', 30, { style: 'dashed' }); const upper = plot.level('Overbought', 70, { style: 'dashed' }); const band = plot.fill('Band', upper, lower, { color: '#9c27b01a' }); chart.onBar(() => { const r = rsi.update(chart.close); history.set(r); if (crossInIf.update(r, 30)) crosses.value += 1; p0.set(r); s0.set(crossInPlot.update(r, 30)); }); ``` ### examples/rsi-pine.pine: RSI cross, pasted Pine The RSI cross as Pine Script, translated when it loads. ```pine //@version=5 // The RSI cross example (rsi-cross.ts) as pasted from TradingView. indicator("RSI cross, pasted Pine", shorttitle="RSI Pine") len = input.int(14, "Length", minval=1) r = ta.rsi(close, len) var int crosses = 0 up = ta.crossover(r, 30) if up crosses += 1 plot(r, "RSI", color=color.purple) plotshape(up, "Up", shape.triangleup, location.bottom, color=color.teal) lower = hline(30, "Oversold", linestyle=hline.style_dashed) upper = hline(70, "Overbought", linestyle=hline.style_dashed) fill(upper, lower, color=color.new(color.purple, 90), title="Band") ``` ### examples/ichimoku-pine.pine: Ichimoku, pasted Pine Ichimoku as Pine Script v6, in TradingView's colours. ```pine //@version=6 // The Ichimoku example (ichimoku.ts) as pasted from TradingView, in TradingView's colours. indicator("Ichimoku cloud, pasted Pine", "Ichimoku Pine", overlay = true) tenkanLength = input.int(9, "Conversion line length", minval = 1) kijunLength = input.int(26, "Base line length", minval = 1) spanBLength = input.int(52, "Leading span B length", minval = 1) shift = input.int(26, "Displacement", minval = 1) midpoint(n) => math.avg(ta.lowest(n), ta.highest(n)) tenkan = midpoint(tenkanLength) kijun = midpoint(kijunLength) spanA = math.avg(tenkan, kijun) spanB = midpoint(spanBLength) plot(tenkan, "Conversion line", color = #2962FF) plot(kijun, "Base line", color = #B71C1C) plot(close, "Lagging span", color = #43A047, offset = -shift + 1) a = plot(spanA, "Leading span A", color = #A5D6A7, offset = shift - 1) b = plot(spanB, "Leading span B", color = #EF9A9A, offset = shift - 1) fill(a, b, color = spanA > spanB ? color.rgb(67, 160, 71, 90) : color.rgb(244, 67, 54, 90), title = "Cloud") ``` ### examples/htf-pine.pine: 4h EMA and daily close, pasted Pine request.security on other timeframes, from pasted Pine. ```pine //@version=5 // Another timeframe in pasted Pine: request.security reads the 4h and daily bars, closed only. indicator("4h EMA and daily close, pasted Pine", shorttitle="HTF Pine", overlay=true) len = input.int(20, "Length", minval=1) ema4h = request.security(syminfo.tickerid, "240", ta.ema(close, len)) daily = request.security(syminfo.tickerid, "D", close) plot(ema4h, "4h EMA", color=color.orange, linewidth=2) plot(daily, "Daily close", color=color.gray, style=plot.style_stepline) ``` ## Built-in indicators ### builtins/accelerator-oscillator.ts ```ts // Accelerator Oscillator (Bill Williams), as TradingView's built-in AC: the Awesome Oscillator less // its own 5-bar simple average, as columns, green while it rises and red while it falls. Reads any // bar type: it needs the high and low. indicator('Accelerator Oscillator', { shortName: 'AC' }); const chart = bars(); const fast = ta.sma(5); const slow = ta.sma(34); const average = ta.sma(5); const previous = state.bar(na); const columns = plot.histogram('AC', { color: color.up, axisLabel: true }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const price = chart.source('hl2'); const ao = fast.update(price) - slow.update(price); const ac = ao - average.update(ao); columns.set(ac, ac - previous.value <= 0 ? color.down : color.up); previous.value = ac; }); ``` ### builtins/accumulation-distribution.ts ```ts // Accumulation/Distribution (Marc Chaikin), as TradingView's built-in (`ta.accdist`): the running // total of each bar's money flow volume, its volume times where the close sits in its range. Its // level depends on how much history is loaded; its slope does not. Needs volume. indicator('Accumulation/Distribution', { shortName: 'Accum/Dist', precision: 0 }); const chart = bars(); const total = state.bar(0); const line = plot.line('Accumulation/Distribution', { color: color.study(10), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { const range = chart.high - chart.low; const ad = (chart.close === chart.high && chart.close === chart.low) || range === 0 ? 0 : ((2 * chart.close - chart.low - chart.high) / range) * chart.volume; if (!isNa(ad)) total.value += ad; line.set(total.value); }); ``` ### builtins/adx.ts ```ts // Average directional index (J. Welles Wilder, 1978), as TradingView's built-in ADX: how strong // the trend is, either way, from 0 to 100. The directional movements and the true range are // smoothed with Wilder's RMA over the DI length, and their normalised difference over the ADX // smoothing. Reads any bar type: it needs the high, low and close. indicator('ADX', { shortName: 'ADX' }); const adxSmoothing = input.int('ADX smoothing', 14, { min: 1, max: 500 }); const diLength = input.int('DI length', 14, { min: 1, max: 500 }); const chart = bars(); const dmi = ta.dmi(diLength, adxSmoothing); const line = plot.line('ADX', { color: color.study(6), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { line.set(dmi.update(chart).adx); }); ``` ### builtins/alligator.ts ```ts // Williams Alligator (Bill Williams, Trading Chaos, 1995), as TradingView's built-in: three // smoothed moving averages of hl2, the jaw (13 bars, drawn 8 ahead), the teeth (8, drawn 5 ahead) // and the lips (5, drawn 3 ahead). Tangled, the market sleeps; fanned out, it trends. Reads any // bar type: it needs the high and low. indicator('Williams Alligator', { overlay: true, shortName: 'Alligator' }); const jawLength = input.int('Jaw length', 13, { min: 1, max: 500 }); const teethLength = input.int('Teeth length', 8, { min: 1, max: 500 }); const lipsLength = input.int('Lips length', 5, { min: 1, max: 500 }); const jawOffset = input.int('Jaw offset', 8, { min: -500, max: 500 }); const teethOffset = input.int('Teeth offset', 5, { min: -500, max: 500 }); const lipsOffset = input.int('Lips offset', 3, { min: -500, max: 500 }); const chart = bars(); const jaw = ta.rma(jawLength); const teeth = ta.rma(teethLength); const lips = ta.rma(lipsLength); const jawLine = plot.line('Jaw', { color: color.study(12), offset: jawOffset }); const teethLine = plot.line('Teeth', { color: color.study(6), offset: teethOffset }); const lipsLine = plot.line('Lips', { color: color.study(4), offset: lipsOffset }); chart.onBar(() => { const price = chart.source('hl2'); jawLine.set(jaw.update(price)); teethLine.set(teeth.update(price)); lipsLine.set(lips.update(price)); }); ``` ### builtins/alma.ts ```ts // Arnaud Legoux moving average, as TradingView's built-in ALMA (`ta.alma`): a Gaussian-weighted // average of the last `length` values, its peak at `offset` of the way from the oldest value to // the newest (0.85 leans it towards recent bars) and its width length / sigma. Na until the window // is full of values. Reads any bar type. indicator('ALMA', { overlay: true, shortName: 'ALMA' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const offset = input.float('Offset', 0.85, { min: 0, max: 1, step: 0.05 }); const sigma = input.float('Sigma', 6, { min: 0.1, step: 0.5 }); const source = input.source('Source', 'close'); // The weights, oldest value first, and their sum, fixed when the script loads. function gaussian() { const m = offset * (length - 1); const s = length / sigma; const w: number[] = []; for (let i = 0; i < length; i += 1) w.push(Math.exp(-((i - m) * (i - m)) / (2 * s * s))); return w; } const weights = gaussian(); const norm = weights.reduce((a, b) => a + b, 0); const chart = bars(); const history = series(length); const line = plot.line('ALMA', { color: color.study(10) }); chart.onBar(() => { history.set(chart.source(source)); let sum = 0; for (let i = 0; i < length; i += 1) sum += history.ago(length - 1 - i) * weights[i]; line.set(sum / norm); }); ``` ### builtins/aroon.ts ```ts // Aroon (Tushar Chande, 1995), as TradingView's built-in: how recently, as a percentage of the // window, the highest high (Aroon Up) and the lowest low (Aroon Down) of the last `length` + 1 // bars were made. 100 is this bar; 0 is `length` bars ago. Reads any bar type: it needs the high // and low. indicator('Aroon', { shortName: 'Aroon' }); const length = input.int('Length', 14, { min: 1, max: 499 }); const chart = bars(); const highest = ta.highestBars(length + 1); const lowest = ta.lowestBars(length + 1); const up = plot.line('Aroon Up', { color: color.study(8), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const down = plot.line('Aroon Down', { color: color.study(12) }); chart.onBar(() => { up.set((100 * (highest.update(chart.high) + length)) / length); down.set((100 * (lowest.update(chart.low) + length)) / length); }); ``` ### builtins/atr-bands.ts ```ts // ATR Bands: bands the multiplier times the average true range (Wilder's, over `length` bars) // above and below the source, the close by default. A volatility-scaled envelope for stops and // targets. Reads any bar type: it needs the high, low and close. indicator('ATR Bands', { overlay: true, shortName: 'ATR Bands' }); const length = input.int('ATR length', 14, { min: 1, max: 500 }); const upperMultiplier = input.float('Upper multiplier', 2, { min: 0, step: 0.1 }); const lowerMultiplier = input.float('Lower multiplier', 2, { min: 0, step: 0.1 }); const source = input.source('Source', 'close'); const chart = bars(); const atr = ta.atr(length); const upper = plot.line('Upper', { color: color.study(11) }); const lower = plot.line('Lower', { color: color.study(11) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(11), 0.08) }); chart.onBar(() => { const a = atr.update(chart); const price = chart.source(source); upper.set(price + upperMultiplier * a); lower.set(price - lowerMultiplier * a); }); ``` ### builtins/atr-trailing-stop.ts ```ts // ATR Trailing Stop: a stop the multiplier times the ATR (Wilder's, over `length` bars) behind the // close, ratcheting while the close stays on its side and flipping to the other side when it // crosses; drawn as a line in the trend's colour. Reads any bar type. indicator('ATR Trailing Stop', { overlay: true, shortName: 'ATR Stop' }); const length = input.int('ATR length', 14, { min: 1, max: 500 }); const multiplier = input.float('Multiplier', 3, { min: 0.1, step: 0.1 }); const chart = bars(); const atr = ta.atr(length); const trail = state.bar(na); const line = plot.line('Stop', { color: color.up, width: 2 }); chart.onBar(() => { const loss = multiplier * atr.update(chart); const price = chart.close; const last = chart.ago(1).close; const before = trail.value; if (isNa(loss)) return; const t = isNa(before) ? price - loss : price > before && last > before ? Math.max(before, price - loss) : price < before && last < before ? Math.min(before, price + loss) : price > before ? price - loss : price + loss; trail.value = t; line.set(t, price >= t ? color.up : color.down); }); ``` ### builtins/atr.ts ```ts // Average true range, as TradingView's built-in ATR: the true range (the largest of the bar's // high less low and its distance from the previous close; on the first bar, high less low), // smoothed. Wilder's RMA by default, which is what `ta.atr` is; the smoothing can be a simple, // exponential or weighted average instead. In price units. Reads any bar type: it needs the // high, low and close. indicator('ATR', { shortName: 'ATR' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const smoothing = input.choice('Smoothing', 'RMA', ['RMA', 'SMA', 'EMA', 'WMA']); // The average, chosen once when the script loads. function smoother(kind: string, n: number) { if (kind === 'SMA') return ta.sma(n); if (kind === 'EMA') return ta.ema(n); if (kind === 'WMA') return ta.wma(n); return ta.rma(n); } // Values are in the market's price units but small beside the price, so they print with the // market's decimals and two more (BTC at a tick of 1 prints 313.24, as it does on a 0.01 tick). function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const range = ta.tr(true); const average = smoother(smoothing, length); const line = plot.line('ATR', { color: color.study(6), precision: decimalsOf(chart.market.tickSize) }); chart.onBar(() => { line.set(average.update(range.update(chart))); }); ``` ### builtins/auto-fib.ts ```ts // Auto Fib Retracement: Fibonacci levels of the last swing, from its start (0) to its end (1), // drawn from the swing's start to the newest bar. Swings are pivots of `depth` bars either side; // a reversal counts only when it moves more than `deviation` times the 10-bar ATR, as a percent // of the price where it is found, as TradingView's Auto Fib scales it. Levels 0, 0.236, 0.382, // 0.5, 0.618, 0.786 and 1, each labelled with its price. Reads any bar type. indicator('Auto Fib Retracement', { overlay: true, shortName: 'Auto Fib', maxLines: 7, maxLabels: 7 }); const deviation = input.float('Deviation', 3, { min: 0.01, step: 0.5 }); const depth = input.int('Depth', 10, { min: 1, max: 250 }); const LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]; const chart = bars(); const pivotHigh = ta.pivotHigh(depth, depth); const pivotLow = ta.pivotLow(depth, depth); const atr = ta.atr(10); const threshold = state.bar(na); // The last two swings: bar and price of the one before (a) and the latest (b). const aBar = state.bar(na); const aPrice = state.bar(na); const bBar = state.bar(na); const bPrice = state.bar(na); // 1 when the latest swing is a high, -1 a low, 0 before the first. const direction = state.bar(0); function swing(price: number, bar: number, high: boolean) { const way = high ? 1 : -1; if (direction.value === 0) { bBar.value = bar; bPrice.value = price; direction.value = way; return; } if (direction.value === way) { if (high ? price > bPrice.value : price < bPrice.value) { bBar.value = bar; bPrice.value = price; } return; } if (!((Math.abs(price - bPrice.value) / bPrice.value) * 100 >= threshold.value)) return; aBar.value = bBar.value; aPrice.value = bPrice.value; bBar.value = bar; bPrice.value = price; direction.value = way; } chart.onBar(() => { const h = pivotHigh.update(chart.high); const l = pivotLow.update(chart.low); threshold.value = ((100 * atr.update(chart)) / chart.close) * deviation; if (!isNa(h)) swing(h, chart.index - depth, true); if (!isNa(l)) swing(l, chart.index - depth, false); if (!chart.isLast || isNa(aPrice.value)) return; for (const level of LEVELS) { const price = aPrice.value + (bPrice.value - aPrice.value) * level; draw.line({ from: { bar: aBar.value, price }, to: { bar: chart.index, price }, color: color.withAlpha(color.study(9), level === 0 || level === 1 ? 0.9 : 0.6) }); draw.label({ at: { bar: chart.index, price }, text: `${level} (${fmt.price(price)})`, style: 'left', color: color.none, textColor: color.study(9), size: 'small' }); } }); ``` ### builtins/average-day-range.ts ```ts // Average Day Range, as TradingView's built-in ADR: the simple average of the bar's high less low // over `length` bars, in price units. On daily bars, the average day's range. Reads any bar type. indicator('Average Day Range', { shortName: 'ADR' }); const length = input.int('Length', 14, { min: 1, max: 500 }); // In price units but small beside the price: the market's decimals and two more. function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const sma = ta.sma(length); const line = plot.line('ADR', { color: color.study(12), precision: decimalsOf(chart.market.tickSize) }); chart.onBar(() => { line.set(sma.update(chart.high - chart.low)); }); ``` ### builtins/awesome-oscillator.ts ```ts // Awesome Oscillator (Bill Williams), as TradingView's built-in AO: the 5-bar simple average of // hl2 less the 34-bar one, as columns, green while it rises and red while it falls. Reads any // bar type: it needs the high and low. indicator('Awesome Oscillator', { shortName: 'AO' }); const chart = bars(); const fast = ta.sma(5); const slow = ta.sma(34); const previous = state.bar(na); const columns = plot.histogram('AO', { color: color.up, axisLabel: true }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const price = chart.source('hl2'); const ao = fast.update(price) - slow.update(price); columns.set(ao, ao - previous.value <= 0 ? color.down : color.up); previous.value = ao; }); ``` ### builtins/balance-of-power.ts ```ts // Balance of Power (Igor Livshin, TASC 2001), as TradingView's built-in BOP: the bar's close less // its open over its high less its low, from −1 to 1. A bar with no range has none. Reads any bar // type. indicator('Balance of Power', { shortName: 'BOP' }); const chart = bars(); const line = plot.line('Balance of Power', { color: color.study(6), range: { min: -1, max: 1 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const range = chart.high - chart.low; line.set(range > 0 ? (chart.close - chart.open) / range : na); }); ``` ### builtins/bb-percent-b.ts ```ts // Bollinger Bands %B (John Bollinger), as TradingView's built-in: where the source sits between the // lower band (0) and the upper band (1) of Bollinger Bands, a 20-bar simple average and two // population standard deviations either side. Reads any bar type. indicator('Bollinger Bands %B', { shortName: 'BB %B' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const multiplier = input.float('StdDev', 2, { min: 0.001, step: 0.1 }); const chart = bars(); const bands = ta.bb(length, multiplier); const line = plot.line('%B', { color: color.study(5), margins: { top: 0.1, bottom: 0.1 } }); const upper = plot.level('Overbought', 1, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle', 0.5, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Oversold', 0, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(5), 0.08) }); chart.onBar(() => { const price = chart.source(source); const b = bands.update(price); line.set((price - b.lower) / (b.upper - b.lower)); }); ``` ### builtins/bb-width.ts ```ts // Bollinger BandWidth (John Bollinger), as TradingView's built-in BBW: the distance between the // bands over the basis, times 100, with the highest and the lowest of it over the last // `expansion length` bars, where a squeeze and an expansion stand out. Reads any bar type. indicator('Bollinger BandWidth', { shortName: 'BBW' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const multiplier = input.float('StdDev', 2, { min: 0.001, step: 0.1 }); const lookback = input.int('Highest expansion length', 125, { min: 1, max: 500 }); const lowLookback = input.int('Lowest contraction length', 125, { min: 1, max: 500 }); const chart = bars(); const bands = ta.bb(length, multiplier); const highest = ta.highest(lookback); const lowest = ta.lowest(lowLookback); const line = plot.line('Bollinger BandWidth', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const high = plot.line('Highest expansion', { color: color.withAlpha(color.down, 0.7), axisLabel: false }); const low = plot.line('Lowest contraction', { color: color.withAlpha(color.up, 0.7), axisLabel: false }); chart.onBar(() => { const b = bands.update(chart.source(source)); const width = ((b.upper - b.lower) / b.basis) * 100; line.set(width); high.set(highest.update(width)); low.set(lowest.update(width)); }); ``` ### builtins/bollinger.ts ```ts // Bollinger Bands, as TradingView's built-in: a simple average of the source, and bands the // standard deviation times the multiplier either side of it. The deviation is `ta.stdev`'s, // over the window's own mean and divided by `length`, not `length - 1` (population, not sample), // as TradingView's is. On the price pane with the space between the bands shaded. Reads any bar // type: it needs only the source, the close by default. indicator('Bollinger Bands', { overlay: true, shortName: 'BB' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const multiplier = input.float('StdDev', 2, { min: 0.001, step: 0.1 }); const chart = bars(); const bands = ta.bb(length, multiplier); const basis = plot.line('Basis', { color: color.study(8) }); const upper = plot.line('Upper', { color: color.study(12) }); const lower = plot.line('Lower', { color: color.study(12) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.1) }); chart.onBar(() => { const value = bands.update(chart.source(source)); basis.set(value.basis); upper.set(value.upper); lower.set(value.lower); }); ``` ### builtins/book-depth-change.ts ```ts // Order book depth change: how much USD the book added or pulled within a band around the mid // since the bar before, bids in the upper half and asks in the lower, solid where liquidity was // added and faint where it was pulled. Each bar reads the book at the bar's end by default, from // 0xArchive's full-depth book archive (Hyperliquid and HIP-3, from 10 March 2026); the newest // hour comes from the live book. A change needs both bars' books. indicator('Book Depth Change', { shortName: 'Depth Chg' }); const band = input.choice('Band', '1%', ['0.1%', '0.25%', '0.5%', '1%', '2.5%', '5%', '10%']); const reading = input.choice('Reading', 'Close', ['Average', 'Close'], { legend: false }); const BANDS: Record = { '0.1%': 10, '0.25%': 25, '0.5%': 50, '1%': 100, '2.5%': 250, '5%': 500, '10%': 1000 }; const bps = BANDS[band] ?? 100; const stat: BookStat = reading === 'Close' ? 'close' : 'avg'; const chart = bars(); const ob = orderBook(); const lastBid = state.bar(na); const lastAsk = state.bar(na); const bidChange = plot.histogram('Bid change', { color: color.up, band: { top: 0.04, bottom: 0.52 }, axisLabel: false }); const askChange = plot.histogram('Ask change', { color: color.down, band: { top: 0.52, bottom: 0.04 }, axisLabel: false }); chart.onBar(() => { const bid = ob.bid(bps, { usd: true, stat }); const ask = ob.ask(bps, { usd: true, stat }); const db = bid - lastBid.value; const da = ask - lastAsk.value; bidChange.set(db, db >= 0 ? color.up : color.withAlpha(color.up, 0.35)); askChange.set(da, da >= 0 ? color.down : color.withAlpha(color.down, 0.35)); }); chart.onClose(() => { lastBid.value = ob.bid(bps, { usd: true, stat }); lastAsk.value = ob.ask(bps, { usd: true, stat }); }); ``` ### builtins/book-depth.ts ```ts // Order book depth: the USD resting on each side within a band around the mid, bids above zero // and asks below. Each bar reads the book over the whole bar, time-weighted, from 0xArchive's // full-depth book archive (Hyperliquid and HIP-3, from 10 March 2026); the newest hour comes from // the live book. A bar the book was not known for stays empty. Book Imbalance reads the same two // sides as one number. indicator('Book Depth', { shortName: 'Depth' }); const band = input.choice('Band', '1%', ['0.1%', '0.25%', '0.5%', '1%', '2.5%', '5%', '10%']); const reading = input.choice('Reading', 'Average', ['Average', 'Close'], { legend: false }); const BANDS: Record = { '0.1%': 10, '0.25%': 25, '0.5%': 50, '1%': 100, '2.5%': 250, '5%': 500, '10%': 1000 }; const bps = BANDS[band] ?? 100; const stat: BookStat = reading === 'Close' ? 'close' : 'avg'; const chart = bars(); const ob = orderBook(); const bids = plot.histogram('Bids', { color: color.up }); const asks = plot.histogram('Asks', { color: color.down }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { bids.set(ob.bid(bps, { usd: true, stat }), color.withAlpha(color.up, 0.6)); asks.set(-ob.ask(bps, { usd: true, stat }), color.withAlpha(color.down, 0.6)); }); ``` ### builtins/book-imbalance.ts ```ts // Order book imbalance: resting bids less asks over their sum, in USD within a band around the // mid, from -100% (all asks) to +100% (all bids). Each bar reads the book over the whole bar, // time-weighted, from 0xArchive's full-depth book archive (Hyperliquid and HIP-3, from 10 March // 2026); the newest hour comes from the live book. A bar the book was not known for stays empty. indicator('Book Imbalance', { shortName: 'Book Imb', precision: 1 }); const band = input.choice('Band', '1%', ['0.1%', '0.25%', '0.5%', '1%', '2.5%', '5%', '10%']); const reading = input.choice('Reading', 'Average', ['Average', 'Close'], { legend: false }); const smoothing = input.int('Smoothing', 1, { min: 1, max: 200, legend: false }); const BANDS: Record = { '0.1%': 10, '0.25%': 25, '0.5%': 50, '1%': 100, '2.5%': 250, '5%': 500, '10%': 1000 }; const bps = BANDS[band] ?? 100; const stat: BookStat = reading === 'Close' ? 'close' : 'avg'; const chart = bars(); const ob = orderBook(); const average = ta.sma(smoothing); const columns = plot.histogram('Imbalance (%)', { color: color.up }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const value = 100 * ob.imbalance(bps, { stat }); // Smoothing is a simple average over that many bars, updated every bar; 1 draws each bar's own reading. const smoothed = average.update(value); const shown = smoothing > 1 ? smoothed : value; columns.set(shown, shown >= 0 ? color.withAlpha(color.up, 0.75) : color.withAlpha(color.down, 0.75)); }); ``` ### builtins/book-market-impact.ts ```ts // Market impact: what a market order of the chosen size pays against the mid, in basis points, // walking the full book at the bar's end, buying through the asks and selling through the bids. // Rising impact is a thinning book. From 0xArchive's full-depth book archive (Hyperliquid and // HIP-3, from 10 March 2026); the newest hour comes from the live book, which may not reach far // enough to fill a large order: those bars stay empty. indicator('Market Impact', { shortName: 'Impact', precision: 2 }); const size = input.choice('Order size', '$100k', ['$10k', '$100k', '$1M']); const USD: Record = { '$10k': 10000, '$100k': 100000, '$1M': 1000000 }; const usd = USD[size] ?? 100000; const chart = bars(); const ob = orderBook(); const buy = plot.line('Buy (bps)', { color: color.up, margins: { top: 0.1, bottom: 0.1 } }); const sell = plot.line('Sell (bps)', { color: color.down }); chart.onBar(() => { buy.set(ob.slippageBuy(usd)); sell.set(ob.slippageSell(usd)); }); ``` ### builtins/book-spread.ts ```ts // Bid-ask spread in basis points of the mid: what crossing the book costs, and how thin it gets. // Each bar reads the spread over the whole bar, time-weighted, from 0xArchive's full-depth book // archive (Hyperliquid and HIP-3, from 10 March 2026); the newest hour comes from the live book. indicator('Book Spread', { shortName: 'Spread', precision: 2 }); const reading = input.choice('Reading', 'Average', ['Average', 'Close'], { legend: false }); const stat: BookStat = reading === 'Close' ? 'close' : 'avg'; const chart = bars(); const ob = orderBook(); const line = plot.line('Spread (bps)', { color: color.study(11), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { line.set(ob.spreadBps({ stat })); }); ``` ### builtins/book-top-imbalance.ts ```ts // Top-of-book imbalance: the size at the best bid less the size at the best ask, over their sum, // from -100% to +100%. The best queues move first and turn fastest; read them with the wider book // imbalance. Each bar reads the book over the whole bar, time-weighted, from 0xArchive's // full-depth book archive (Hyperliquid and HIP-3, from 10 March 2026); the newest hour comes from // the live book. indicator('Top-of-Book Imbalance', { shortName: 'TOB Imb', precision: 1 }); const reading = input.choice('Reading', 'Average', ['Average', 'Close'], { legend: false }); const smoothing = input.int('Smoothing', 1, { min: 1, max: 200, legend: false }); const stat: BookStat = reading === 'Close' ? 'close' : 'avg'; const chart = bars(); const ob = orderBook(); const average = ta.sma(smoothing); const columns = plot.histogram('Imbalance (%)', { color: color.up }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const bid = ob.bestBidSize({ stat }); const ask = ob.bestAskSize({ stat }); const value = bid + ask > 0 ? (100 * (bid - ask)) / (bid + ask) : na; // Smoothing is a simple average over that many bars, updated every bar; 1 draws each bar's own reading. const smoothed = average.update(value); const shown = smoothing > 1 ? smoothed : value; columns.set(shown, shown >= 0 ? color.withAlpha(color.up, 0.75) : color.withAlpha(color.down, 0.75)); }); ``` ### builtins/breadth.ts ```ts // The share of a venue's markets trading above their session VWAP, from 0xArchive: data no // other charting tool has. A value is drawn on the bar its latest sample falls in. indicator('VWAP breadth', { precision: 2 }); const universe = input.choice('Universe', 'hl', ['hl', 'hip3']); const showCoverage = input.bool('Coverage', true); const chart = bars(); const b = breadth({ venue: universe }); const value = plot.line('Above VWAP', { width: 2, range: { min: 0, max: 100 }, margins: { top: 0.22, bottom: 0.08 } }); const coverage = plot.line('Coverage', { width: 1, style: 'dotted', range: { min: 0, max: 100 }, axisLabel: false, hidden: !showCoverage }); plot.level('Midline', 50, { style: 'dotted' }); chart.onBar(() => { // Only a sample calculated inside this bar: bars with none stay empty, as on the chart today. if (!b.available || b.time < chart.time) return; value.set(b.value); if (showCoverage) coverage.set(b.coverage); }); ``` ### builtins/bull-bear-power.ts ```ts // Bull Bear Power (Alexander Elder's Elder-ray), as TradingView's built-in: bull power (the high // less a 13-bar EMA of the close) plus bear power (the low less the same EMA), as columns. Reads // any bar type: it needs the high, low and close. indicator('Bull Bear Power', { shortName: 'BBPower' }); const length = input.int('Length', 13, { min: 1, max: 500 }); const chart = bars(); const ema = ta.ema(length); const columns = plot.histogram('BBPower', { color: color.up, axisLabel: true }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const average = ema.update(chart.close); const power = chart.high - average + (chart.low - average); columns.set(power, power >= 0 ? color.withAlpha(color.up, 0.6) : color.withAlpha(color.down, 0.6)); }); ``` ### builtins/cci.ts ```ts // Commodity channel index (Donald Lambert, 1980), as TradingView's built-in CCI: the source's // distance from its `length`-bar mean, over 0.015 times the mean absolute deviation. Bands at +100 // and −100. Reads any bar type. indicator('CCI', { shortName: 'CCI' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'hlc3'); const chart = bars(); const cci = ta.cci(length); const line = plot.line('CCI', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const upper = plot.level('Upper band', 100, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', -100, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { line.set(cci.update(chart.source(source))); }); ``` ### builtins/center-of-gravity.ts ```ts // Center of Gravity (John Ehlers, TASC May 2002): where the weight of the last `length` values of // hl2 sits, newest weighted 1 and oldest `length`, against the window's middle; it turns with // little lag at cycle turns. Its trigger is the value one bar back. Reads any bar type. indicator('Ehlers Center of Gravity', { shortName: 'CG' }); const length = input.int('Length', 10, { min: 2, max: 500 }); const source = input.source('Source', 'hl2'); const chart = bars(); const price = series(length); const cg = series(1); const cgLine = plot.line('CG', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const triggerLine = plot.line('Trigger', { color: color.study(8) }); chart.onBar(() => { price.set(chart.source(source)); let num = 0; let den = 0; for (let k = 0; k < length; k += 1) { const v = price.ago(k); num += (1 + k) * v; den += v; } cg.set(den !== 0 ? -num / den + (length + 1) / 2 : na); cgLine.set(cg.value); triggerLine.set(cg.ago(1)); }); ``` ### builtins/chaikin-oscillator.ts ```ts // Chaikin Oscillator (Marc Chaikin), as TradingView's built-in: the 3-bar EMA of the // Accumulation/Distribution line less its 10-bar EMA. Needs volume. indicator('Chaikin Oscillator', { shortName: 'Chaikin Osc', precision: 0 }); const fastLength = input.int('Fast length', 3, { min: 1, max: 500 }); const slowLength = input.int('Slow length', 10, { min: 1, max: 500 }); const chart = bars(); const total = state.bar(0); const fast = ta.ema(fastLength); const slow = ta.ema(slowLength); const line = plot.line('Chaikin Oscillator', { color: color.study(6), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const range = chart.high - chart.low; const ad = (chart.close === chart.high && chart.close === chart.low) || range === 0 ? 0 : ((2 * chart.close - chart.low - chart.high) / range) * chart.volume; if (!isNa(ad)) total.value += ad; line.set(fast.update(total.value) - slow.update(total.value)); }); ``` ### builtins/chaikin-volatility.ts ```ts // Chaikin Volatility (Marc Chaikin): the percent change, over `roc length` bars, of the EMA of the // bar's range over `length` bars. Rising, ranges widen; falling, they narrow. Reads any bar type: // it needs the high and low. indicator('Chaikin Volatility', { shortName: 'ChaikinVol' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const rocLength = input.int('ROC length', 10, { min: 1, max: 500 }); const chart = bars(); const ema = ta.ema(length); const roc = ta.roc(rocLength); const line = plot.line('Chaikin Volatility', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(roc.update(ema.update(chart.high - chart.low))); }); ``` ### builtins/chande-kroll-stop.ts ```ts // Chande Kroll Stop (Tushar Chande and Stanley Kroll, The New Technical Trader, 1994), as // TradingView's built-in: a first stop the multiplier times the ATR (Wilder's, over `p` bars) // inside the highest high and the lowest low of `p` bars, then the outermost of those over `q` // bars. Longs stop below Stop long, shorts above Stop short. Reads any bar type: it needs the // high, low and close. indicator('Chande Kroll Stop', { overlay: true, shortName: 'CKS' }); const p = input.int('P', 10, { min: 1, max: 500 }); const x = input.float('X', 1, { min: 0, step: 0.1 }); const q = input.int('Q', 9, { min: 1, max: 500 }); const chart = bars(); const atr = ta.atr(p); const highest = ta.highest(p); const lowest = ta.lowest(p); const shortStop = ta.highest(q); const longStop = ta.lowest(q); const longLine = plot.line('Stop long', { color: color.up }); const shortLine = plot.line('Stop short', { color: color.down }); chart.onBar(() => { const range = atr.update(chart) * x; const firstHigh = highest.update(chart.high) - range; const firstLow = lowest.update(chart.low) + range; shortLine.set(shortStop.update(firstHigh)); longLine.set(longStop.update(firstLow)); }); ``` ### builtins/chandelier-exit.ts ```ts // Chandelier Exit (Charles LeBeau): a stop the multiplier times the ATR (Wilder's, over `length` // bars) below the highest high of those bars for longs, above the lowest low for shorts. Each stop // only ratchets towards price while the previous close stays on its side, and the trend flips when // the close crosses the other side's stop; the line shown is the stop of the trend. Optionally // the extremes are of the close rather than the high and low. Reads any bar type. indicator('Chandelier Exit', { overlay: true, shortName: 'CE' }); const length = input.int('ATR period', 22, { min: 1, max: 500 }); const multiplier = input.float('ATR multiplier', 3, { min: 0.1, step: 0.1 }); const useClose = input.bool('Use close price for extremums', false); const chart = bars(); const atr = ta.atr(length); const highest = ta.highest(length); const lowest = ta.lowest(length); const longStop = state.bar(na); const shortStop = state.bar(na); const direction = state.bar(1); const longLine = plot.line('Long stop', { color: color.up }); const shortLine = plot.line('Short stop', { color: color.down }); chart.onBar(() => { const range = atr.update(chart) * multiplier; const top = highest.update(useClose ? chart.close : chart.high); const bottom = lowest.update(useClose ? chart.close : chart.low); if (isNa(range) || isNa(top) || isNa(bottom)) return; const previousClose = chart.ago(1).close; const longBefore = isNa(longStop.value) ? top - range : longStop.value; const shortBefore = isNa(shortStop.value) ? bottom + range : shortStop.value; longStop.value = previousClose > longBefore ? Math.max(top - range, longBefore) : top - range; shortStop.value = previousClose < shortBefore ? Math.min(bottom + range, shortBefore) : bottom + range; direction.value = chart.close > shortBefore ? 1 : chart.close < longBefore ? -1 : direction.value; if (direction.value === 1) longLine.set(longStop.value); else shortLine.set(shortStop.value); }); ``` ### builtins/choppiness.ts ```ts // Choppiness index (E. W. Dreiss), as TradingView's built-in: 100 × log10(sum of the true range // over `length` bars ÷ the range of those bars) ÷ log10(length). Near 100 the market chops // sideways; near 0 it trends. Bands at 61.8 and 38.2. Reads any bar type: it needs the high, // low and close. indicator('Choppiness Index', { shortName: 'CHOP' }); const length = input.int('Length', 14, { min: 2, max: 500 }); const chart = bars(); const ranges = ta.sma(length); const trueRange = ta.tr(true); const highest = ta.highest(length); const lowest = ta.lowest(length); const line = plot.line('CHOP', { color: color.study(12), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 61.8, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const lower = plot.level('Lower band', 38.2, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { const sum = ranges.update(trueRange.update(chart)) * length; const span = highest.update(chart.high) - lowest.update(chart.low); line.set((100 * Math.log10(sum / span)) / Math.log10(length)); }); ``` ### builtins/cmf.ts ```ts // Chaikin Money Flow (Marc Chaikin), as TradingView's built-in CMF: the money flow volume (volume // times where the close sits in the bar's range, from −1 at the low to 1 at the high) summed over // `length` bars, over the volume of those bars. A bar with no range adds none. Needs volume. indicator('Chaikin Money Flow', { shortName: 'CMF' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const chart = bars(); const flow = ta.sma(length); const volume = ta.sma(length); const line = plot.line('MF', { color: color.study(4), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const range = chart.high - chart.low; const ad = (chart.close === chart.high && chart.close === chart.low) || range === 0 ? 0 : ((2 * chart.close - chart.low - chart.high) / range) * chart.volume; // Sums as averages: the length cancels. line.set(flow.update(ad) / volume.update(chart.volume)); }); ``` ### builtins/cmo.ts ```ts // Chande Momentum Oscillator (Tushar Chande, 1994), as TradingView's built-in CMO: the sum of the // gains over `length` bars less the sum of the losses, over their total, times 100. From −100 to // 100. Reads any bar type. indicator('Chande Momentum Oscillator', { shortName: 'ChandeMO' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const change = ta.change(1); const gains = ta.sma(length); const losses = ta.sma(length); const line = plot.line('Chande MO', { color: color.study(12), range: { min: -100, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const m = change.update(chart.source(source)); const up = gains.update(m >= 0 ? m : isNa(m) ? na : 0); const down = losses.update(m >= 0 ? 0 : isNa(m) ? na : -m); line.set((100 * (up - down)) / (up + down)); }); ``` ### builtins/connors-rsi.ts ```ts // Connors RSI (Larry Connors), as TradingView's built-in CRSI: the mean of a 3-bar RSI of the // source, a 2-bar RSI of its up/down streak (how many bars in a row it rose, positive, or fell, // negative; 0 when unchanged) and the percent rank of its one-bar rate of change over 100 bars. // Bands at 70 and 30. Reads any bar type. indicator('Connors RSI', { shortName: 'CRSI' }); const rsiLength = input.int('RSI length', 3, { min: 1, max: 500 }); const streakLength = input.int('UpDown length', 2, { min: 1, max: 500 }); const rocLength = input.int('ROC length', 100, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const history = series(1); const streak = state.bar(0); const rsi = ta.rsi(rsiLength); const streakRsi = ta.rsi(streakLength); const roc = ta.roc(1); const rank = ta.percentRank(rocLength); const line = plot.line('CRSI', { color: color.study(13), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 70, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const lower = plot.level('Lower band', 30, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(13), 0.08) }); chart.onBar(() => { const price = chart.source(source); history.set(price); const previous = history.ago(1); // As TradingView's: on the first bar, with no previous value, the streak is −1. const s = streak.value; streak.value = price === previous ? 0 : price > previous ? (s <= 0 ? 1 : s + 1) : (s >= 0 ? -1 : s - 1); const a = rsi.update(price); const b = streakRsi.update(streak.value); const c = rank.update(roc.update(price)); line.set((a + b + c) / 3); }); ``` ### builtins/coppock.ts ```ts // Coppock Curve (Edwin Coppock, 1962), as TradingView's built-in: a 10-bar weighted average of the // sum of the 14-bar and the 11-bar rates of change. Built for monthly bars; it reads any. A zero // line. indicator('Coppock Curve', { shortName: 'Coppock' }); const wmaLength = input.int('WMA length', 10, { min: 1, max: 500 }); const longLength = input.int('Long RoC length', 14, { min: 1, max: 500 }); const shortLength = input.int('Short RoC length', 11, { min: 1, max: 500 }); const chart = bars(); const longRoc = ta.roc(longLength); const shortRoc = ta.roc(shortLength); const wma = ta.wma(wmaLength); const line = plot.line('Coppock Curve', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(wma.update(longRoc.update(chart.close) + shortRoc.update(chart.close))); }); ``` ### builtins/coral-trend.ts ```ts // Coral Trend, as LazyBear described it: six EMAs in a chain over (period − 1) / 2 + 1 bars, // combined like Tillson's T3 with the constant D, coloured up while it rises and down while it // falls. Every EMA starts at the first price. Written from the description; no code copied. indicator('Coral Trend', { overlay: true, shortName: 'Coral' }); const period = input.int('Smoothing period', 21, { min: 1, max: 500 }); const constantD = input.float('Constant D', 0.4, { min: 0, max: 1, step: 0.05 }); const source = input.source('Source', 'close'); const di = (period - 1) / 2 + 1; const c1 = 2 / (di + 1); const c2 = 1 - c1; const d = constantD; const c3 = 3 * (d * d + d * d * d); const c4 = -3 * (2 * d * d + d + d * d * d); const c5 = 3 * d + 1 + d * d * d + 3 * d * d; const chart = bars(); const stages = state.list(6); const previous = state.bar(na); const line = plot.line('Coral', { color: color.up, width: 2 }); chart.onBar(() => { const x = chart.source(source); if (stages.length === 0) for (let k = 0; k < 6; k += 1) stages.push(x); let input = x; for (let k = 0; k < 6; k += 1) { const v = c1 * input + c2 * stages.get(k); stages.set(k, v); input = v; } const coral = -d * d * d * stages.get(5) + c3 * stages.get(4) + c4 * stages.get(3) + c5 * stages.get(2); line.set(coral, isNa(previous.value) || coral >= previous.value ? color.up : color.down); previous.value = coral; }); ``` ### builtins/correlation-trend.ts ```ts // Correlation Trend Indicator (John Ehlers, TASC May 2020): the Pearson correlation of the close // with a straight line rising over the last `length` bars, from −1 (falling throughout) to 1 // (rising throughout). Reads any bar type. indicator('Correlation Trend Indicator', { shortName: 'CTI' }); const length = input.int('Length', 20, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const price = series(length); const line = plot.line('CTI', { color: color.study(12), range: { min: -1, max: 1 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { price.set(chart.source(source)); if (chart.index < length - 1) return; // y is the line, −k for the value k bars back, as Ehlers' code counts. let sx = 0, sy = 0, sxx = 0, sxy = 0, syy = 0; for (let k = 0; k < length; k += 1) { const x = price.ago(k); const y = -k; sx += x; sy += y; sxx += x * x; sxy += x * y; syy += y * y; } const d = (length * sxx - sx * sx) * (length * syy - sy * sy); line.set(d > 0 ? (length * sxy - sx * sy) / Math.sqrt(d) : 0); }); ``` ### builtins/cvd-tape.ts ```ts // Cumulative volume delta from the tape: every taker fill, buys minus sells, in USD. Trade // history is limited, so bars the tape does not cover completely have no value. indicator('CVD (tape)'); const deltaBars = input.bool('Delta bars', true); const chart = bars(); const tape = trades(); const delta = tally(); const total = state.bar(0); const cvd = plot.line('CVD', { width: 2, margins: { top: 0.2, bottom: 0.28 } }); const deltaPlot = plot.histogram('Delta', { band: { top: 0.68, bottom: 0.02 }, axisLabel: false, hidden: !deltaBars }); tape.onTrade((t) => delta.add(t.side === 'buy' ? t.notional : -t.notional)); chart.onBar(() => { if (!tape.available) return; cvd.set(total.value + delta.value); if (deltaBars) deltaPlot.set(delta.value, delta.value >= 0 ? color.up : color.down); }); chart.onClose(() => { if (tape.available) total.value += delta.value; }); ``` ### builtins/cvd.ts ```ts // Cumulative volume delta from each bar's taker buy and sell volume: the delta per bar, summed // from the first loaded bar. Its level therefore depends on how much history is loaded; its shape // does not. A bar without both volumes adds nothing. indicator('CVD'); const deltaBars = input.bool('Delta bars', true); const chart = bars(); const total = state.bar(0); const cvd = plot.line('CVD', { width: 2, margins: { top: 0.2, bottom: 0.28 } }); const delta = plot.histogram('Delta', { band: { top: 0.68, bottom: 0.02 }, axisLabel: false, hidden: !deltaBars }); chart.onBar(() => { const d = chart.buyVolume - chart.sellVolume; if (isNa(d)) return; cvd.set(total.value + d); if (deltaBars) delta.set(d, d >= 0 ? color.up : color.down); }); chart.onClose(() => { const d = chart.buyVolume - chart.sellVolume; if (!isNa(d)) total.value += d; }); ``` ### builtins/cyber-cycle.ts ```ts // Cyber Cycle (John Ehlers, Cybernetic Analysis, 2004): a high-pass filter of a four-bar smoothing // of hl2 that keeps the cycle and removes the trend, and its trigger, the cycle one bar back. For // the first seven bars it is a second difference of the price, as Ehlers' code starts. Reads any // bar type. indicator('Ehlers Cyber Cycle', { shortName: 'CyberCycle' }); const alpha = input.float('Alpha', 0.07, { min: 0.01, max: 1, step: 0.01 }); const source = input.source('Source', 'hl2'); const chart = bars(); const price = series(3); const smooth = series(2); const cycle = series(2); const cycleLine = plot.line('Cycle', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const triggerLine = plot.line('Trigger', { color: color.study(8) }); chart.onBar(() => { const x = chart.source(source); price.set(x); smooth.set((x + 2 * nz(price.ago(1), x) + 2 * nz(price.ago(2), x) + nz(price.ago(3), x)) / 6); if (chart.index < 7) cycle.set(chart.index < 2 ? 0 : (x - 2 * price.ago(1) + price.ago(2)) / 4); else cycle.set((1 - 0.5 * alpha) ** 2 * (smooth.value - 2 * smooth.ago(1) + smooth.ago(2)) + 2 * (1 - alpha) * cycle.ago(1) - (1 - alpha) ** 2 * cycle.ago(2)); cycleLine.set(cycle.value); triggerLine.set(cycle.ago(1)); }); ``` ### builtins/decycler-oscillator.ts ```ts // Decycler Oscillator (John Ehlers, TASC September 2015): the difference of two decyclers, which is // a two-pole high-pass filter at the slow period less one at the fast period, as a percentage of // the price. Above zero the trend is up. Its first two values are zero, as Ehlers' code starts. // Reads any bar type. indicator('Ehlers Decycler Oscillator', { shortName: 'DecyclerOsc' }); const fastPeriod = input.int('Fast period', 30, { min: 2, max: 500 }); const slowPeriod = input.int('Slow period', 60, { min: 2, max: 500 }); const source = input.source('Source', 'close'); function alphaOf(period: number) { const w = (0.707 * 2 * Math.PI) / period; return (Math.cos(w) + Math.sin(w) - 1) / Math.cos(w); } const fastAlpha = alphaOf(fastPeriod); const slowAlpha = alphaOf(slowPeriod); const chart = bars(); const price = series(2); const fast = series(2); const slow = series(2); const line = plot.line('Decycler Oscillator', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const x = chart.source(source); price.set(x); if (chart.index < 2) { fast.set(0); slow.set(0); line.set(0); return; } const d = x - 2 * price.ago(1) + price.ago(2); fast.set((1 - fastAlpha / 2) ** 2 * d + 2 * (1 - fastAlpha) * fast.ago(1) - (1 - fastAlpha) ** 2 * fast.ago(2)); slow.set((1 - slowAlpha / 2) ** 2 * d + 2 * (1 - slowAlpha) * slow.ago(1) - (1 - slowAlpha) ** 2 * slow.ago(2)); line.set((100 * (slow.value - fast.value)) / x); }); ``` ### builtins/decycler.ts ```ts // Decycler (John Ehlers, TASC September 2015): the price less its cycles shorter than the cutoff, // by a one-pole high-pass filter taken away, which leaves the trend with very little lag. It // starts at the first price. Reads any bar type. indicator('Ehlers Decycler', { overlay: true, shortName: 'Decycler' }); const cutoff = input.int('Cutoff period', 125, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const w = (2 * Math.PI) / cutoff; const alpha = (Math.cos(w) + Math.sin(w) - 1) / Math.cos(w); const chart = bars(); const price = series(1); const decycle = state.bar(na); const line = plot.line('Decycler', { color: color.study(8) }); chart.onBar(() => { const x = chart.source(source); price.set(x); decycle.value = chart.index === 0 ? x : (alpha / 2) * (x + price.ago(1)) + (1 - alpha) * decycle.value; line.set(decycle.value); }); ``` ### builtins/dema.ts ```ts // Double exponential moving average (Patrick Mulloy, TASC 1994), as TradingView's built-in DEMA: // 2 × EMA − EMA of that EMA, both over `length`. Each EMA is seeded with a simple average, as // `ta.ema` is, so the first value is on bar 2 × (length − 1). Reads any bar type. indicator('DEMA', { overlay: true, shortName: 'DEMA' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const e1 = ta.ema(length); const e2 = ta.ema(length); const line = plot.line('DEMA', { color: color.study(14) }); chart.onBar(() => { const a = e1.update(chart.source(source)); const b = e2.update(a); line.set(2 * a - b); }); ``` ### builtins/dmi.ts ```ts // Directional movement index (J. Welles Wilder, 1978), as TradingView's built-in DMI: +DI and −DI, // the smoothed up and down moves as a percentage of the smoothed true range, and the ADX, how // strong the trend is either way. Wilder's RMA throughout. Reads any bar type: it needs the high, // low and close. indicator('DMI', { shortName: 'DMI' }); const adxSmoothing = input.int('ADX smoothing', 14, { min: 1, max: 500 }); const diLength = input.int('DI length', 14, { min: 1, max: 500 }); const chart = bars(); const dmi = ta.dmi(diLength, adxSmoothing); const adx = plot.line('ADX', { color: color.study(6), margins: { top: 0.1, bottom: 0.1 } }); const plus = plot.line('+DI', { color: color.study(12) }); const minus = plot.line('−DI', { color: color.study(8) }); chart.onBar(() => { const value = dmi.update(chart); adx.set(value.adx); plus.set(value.plus); minus.set(value.minus); }); ``` ### builtins/dominant-cycle.ts ```ts // Dominant cycle period (John Ehlers, Rocket Science for Traders, 2001): the period, in bars, of // the price's dominant cycle, measured by a Hilbert transform's homodyne discriminator and // smoothed, between 6 and 50 bars. As TA-Lib's HT_DCPERIOD in method, and Ehlers' EasyLanguage in // its start: arctangents in degrees, every series starting at zero, the transform running from // the sixth bar. Drawn from bar 32, TA-Lib's warm-up. Reads any bar type. indicator('Dominant Cycle Period', { shortName: 'DC Period' }); const source = input.source('Source', 'hl2'); const DEG = 180 / Math.PI; const chart = bars(); const price = series(3); const smooth = series(6); const detrender = series(6); const i1 = series(6); const q1 = series(6); const i2 = state.bar(0); const q2 = state.bar(0); const re = state.bar(0); const im = state.bar(0); const period = state.bar(0); const smoothPeriod = state.bar(0); const line = plot.line('Period', { color: color.study(13), margins: { top: 0.1, bottom: 0.1 } }); // Ehlers' Hilbert transform kernel over a series' last seven values. function hilbert(s: Series, gain: number) { return (0.0962 * s.value + 0.5769 * nz(s.ago(2)) - 0.5769 * nz(s.ago(4)) - 0.0962 * nz(s.ago(6))) * gain; } chart.onBar(() => { const x = chart.source(source); price.set(x); if (chart.index < 5) { smooth.set(0); detrender.set(0); i1.set(0); q1.set(0); return; } const gain = 0.075 * period.value + 0.54; smooth.set((4 * x + 3 * price.ago(1) + 2 * price.ago(2) + price.ago(3)) / 10); detrender.set(hilbert(smooth, gain)); q1.set(hilbert(detrender, gain)); i1.set(nz(detrender.ago(3))); const jI = hilbert(i1, gain); const jQ = hilbert(q1, gain); const lastI2 = i2.value; const lastQ2 = q2.value; i2.value = 0.2 * (i1.value - jQ) + 0.8 * lastI2; q2.value = 0.2 * (q1.value + jI) + 0.8 * lastQ2; re.value = 0.2 * (i2.value * lastI2 + q2.value * lastQ2) + 0.8 * re.value; im.value = 0.2 * (i2.value * lastQ2 - q2.value * lastI2) + 0.8 * im.value; const before = period.value; let p = before; if (im.value !== 0 && re.value !== 0) p = 360 / (Math.atan(im.value / re.value) * DEG); if (p > 1.5 * before) p = 1.5 * before; if (p < 0.67 * before) p = 0.67 * before; if (p < 6) p = 6; if (p > 50) p = 50; period.value = 0.2 * p + 0.8 * before; smoothPeriod.value = 0.33 * period.value + 0.67 * smoothPeriod.value; if (chart.index >= 32) line.set(smoothPeriod.value); }); ``` ### builtins/donchian.ts ```ts // Donchian Channels, as TradingView's built-in: the highest high and the lowest low of the last // `length` bars (this bar included), and their midpoint. On the price pane with the channel // shaded. Reads any bar type: it needs the high and the low. indicator('Donchian Channels', { overlay: true, shortName: 'DC' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const chart = bars(); const highest = ta.highest(length); const lowest = ta.lowest(length); const basis = plot.line('Basis', { color: color.study(8) }); const upper = plot.line('Upper', { color: color.study(12) }); const lower = plot.line('Lower', { color: color.study(12) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.1) }); chart.onBar(() => { const top = highest.update(chart.high); const bottom = lowest.update(chart.low); upper.set(top); lower.set(bottom); basis.set((top + bottom) / 2); }); ``` ### builtins/dpo.ts ```ts // Detrended Price Oscillator, as TradingView's built-in DPO: the close less its `period`-bar simple // average as it was period / 2 + 1 bars ago, which strips out the trend and leaves the cycles. // Centred, it is the close of period / 2 + 1 bars ago less today's average, drawn that many bars // back. Reads any bar type. indicator('Detrended Price Oscillator', { shortName: 'DPO' }); const period = input.int('Length', 21, { min: 1, max: 500 }); const centered = input.bool('Centered', false); const back = Math.floor(period / 2) + 1; const chart = bars(); const sma = ta.sma(period); const averages = series(back); const line = plot.line('DPO', { color: color.study(10), offset: centered ? -back : 0, margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const average = sma.update(chart.close); averages.set(average); line.set(centered ? chart.ago(back).close - average : chart.close - averages.ago(back)); }); ``` ### builtins/ease-of-movement.ts ```ts // Ease of Movement (Richard Arms), as TradingView's built-in EOM: the move of hl2 times the bar's // range over its volume, scaled by the divisor and averaged over `length` bars. High where price // moves far on little volume. Needs volume. indicator('Ease of Movement', { shortName: 'EOM' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const divisor = input.int('Divisor', 10000, { min: 1 }); const chart = bars(); const change = ta.change(1); const sma = ta.sma(length); const line = plot.line('EOM', { color: color.study(5), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(sma.update((divisor * change.update(chart.source('hl2')) * (chart.high - chart.low)) / chart.volume)); }); ``` ### builtins/ehlers-stochastic.ts ```ts // Ehlers Stochastic (John Ehlers, Cycle Analytics for Traders, 2013): the stochastic of a roofing // filter of the close (cycles between 10 and 48 bars) over `length` bars, smoothed by a Super // Smoother, from 0 to 100. Bands at 80 and 20. Its filters' first two values are zero, as Ehlers' // code starts. Reads any bar type. indicator('Ehlers Stochastic', { shortName: 'EStoch' }); const length = input.int('Length', 20, { min: 2, max: 500 }); const w = (0.707 * 2 * Math.PI) / 48; const alpha = (Math.cos(w) + Math.sin(w) - 1) / Math.cos(w); const a1 = Math.exp((-1.414 * Math.PI) / 10); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / 10); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const price = series(2); const hp = series(2); const filt = series(length); const stoc = series(1); const out = series(2); const line = plot.line('Stochastic', { color: color.study(12), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const lower = plot.level('Lower band', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { const x = chart.close; price.set(x); if (chart.index < 2) { hp.set(0); filt.set(0); stoc.set(0); out.set(0); return; } hp.set((1 - alpha / 2) ** 2 * (x - 2 * price.ago(1) + price.ago(2)) + 2 * (1 - alpha) * hp.ago(1) - (1 - alpha) ** 2 * hp.ago(2)); filt.set((c1 * (hp.value + hp.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); let hi = -Infinity; let lo = Infinity; for (let k = 0; k < Math.min(length, chart.index + 1); k += 1) { const v = nz(filt.ago(k)); hi = Math.max(hi, v); lo = Math.min(lo, v); } stoc.set(hi > lo ? (filt.value - lo) / (hi - lo) : 0); out.set((c1 * (stoc.value + stoc.ago(1))) / 2 + c2 * out.ago(1) + c3 * out.ago(2)); if (chart.index >= length) line.set(100 * out.value); }); ``` ### builtins/elder-force-index.ts ```ts // Elder Force Index (Alexander Elder), as TradingView's built-in EFI: the close's change times the // bar's volume, smoothed by a 13-bar EMA. Needs volume. indicator('Elder Force Index', { shortName: 'EFI', precision: 0 }); const length = input.int('Length', 13, { min: 1, max: 500 }); const chart = bars(); const change = ta.change(1); const ema = ta.ema(length); const line = plot.line('Elder Force Index', { color: color.study(6), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(ema.update(change.update(chart.close) * chart.volume)); }); ``` ### builtins/ema.ts ```ts // Exponential moving average, the chart's built-in EMA. The chart opens with two instances, // lengths 20 and 50. ta.ema follows TradingView: it is seeded with the simple average of the // first `length` values, so the first bars of loaded history differ from the old chart EMA, // which started from the first bar's value (docs/scripting.md §9). indicator('EMA', { overlay: true }); const length = input.int('Length', 20, { min: 2, max: 500, step: 1 }); const source = input.choice('Source', 'close', ['close', 'open', 'high', 'low', 'hl2', 'hlc3', 'ohlc4']); const chart = bars(); const ema = ta.ema(length); const line = plot.line(`EMA ${length}`, { width: 1, style: 'solid' }); chart.onBar(() => { line.set(ema.update(chart.source(source))); }); ``` ### builtins/envelopes.ts ```ts // Envelope, as TradingView's built-in: a moving average of the source (simple by default) and // bands a fixed percentage above and below it. Reads any bar type. indicator('Envelopes', { overlay: true, shortName: 'Env' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const percent = input.float('Percent', 10, { min: 0, step: 0.5 }); const source = input.source('Source', 'close'); const exponential = input.bool('Exponential', false); const chart = bars(); const sma = ta.sma(length); const ema = ta.ema(length); const basis = plot.line('Basis', { color: color.study(8) }); const upper = plot.line('Upper', { color: color.study(12) }); const lower = plot.line('Lower', { color: color.study(12) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { const price = chart.source(source); const s = sma.update(price); const e = ema.update(price); const middle = exponential ? e : s; basis.set(middle); upper.set(middle * (1 + percent / 100)); lower.set(middle * (1 - percent / 100)); }); ``` ### builtins/even-better-sinewave.ts ```ts // Even Better Sinewave (John Ehlers, Cycle Analytics for Traders, 2013): a one-pole high-pass // filter of the close over `duration` bars, smoothed by a Super Smoother, then its three-bar // average over its three-bar root mean square: a wave between −1 and 1 that flattens at the // extremes in a trend. Its first value is zero. Reads any bar type. indicator('Even Better Sinewave', { shortName: 'EBSW' }); const duration = input.int('Duration', 40, { min: 2, max: 500 }); const smoothing = input.int('Smoothing', 10, { min: 2, max: 500 }); const w = (2 * Math.PI) / duration; const alpha = (1 - Math.sin(w)) / Math.cos(w); const a1 = Math.exp((-1.414 * Math.PI) / smoothing); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / smoothing); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const hp = series(1); const filt = series(2); const line = plot.line('Wave', { color: color.study(12), range: { min: -1, max: 1 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const previous = chart.ago(1).close; hp.set(chart.index === 0 ? 0 : 0.5 * (1 + alpha) * (chart.close - previous) + alpha * hp.ago(1)); filt.set(chart.index < 2 ? hp.value : (c1 * (hp.value + hp.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); if (chart.index < 2) return; const wave = (filt.value + filt.ago(1) + filt.ago(2)) / 3; const power = (filt.value ** 2 + filt.ago(1) ** 2 + filt.ago(2) ** 2) / 3; line.set(power > 0 ? wave / Math.sqrt(power) : 0); }); ``` ### builtins/fisher-transform.ts ```ts // Fisher Transform (John Ehlers, TASC 2002), as TradingView's built-in: hl2's place in its // `length`-bar range, smoothed and kept inside ±0.999, then its Fisher transform, also smoothed. // The trigger is the Fisher of the bar before. Levels at ±1.5 and ±0.75. Reads any bar type: it // needs the high and low. indicator('Fisher Transform', { shortName: 'Fisher' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const chart = bars(); const highest = ta.highest(length); const lowest = ta.lowest(length); const value = state.bar(na); const fisher = state.bar(na); const fisherLine = plot.line('Fisher', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const triggerLine = plot.line('Trigger', { color: color.study(8) }); plot.level('1.5', 1.5, { style: 'dashed', color: color.withAlpha(color.neutral, 0.35) }); plot.level('0.75', 0.75, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('0', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('−0.75', -0.75, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('−1.5', -1.5, { style: 'dashed', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const price = chart.source('hl2'); const top = highest.update(price); const bottom = lowest.update(price); const raw = 0.66 * ((price - bottom) / (top - bottom) - 0.5) + 0.67 * nz(value.value); value.value = raw > 0.99 ? 0.999 : raw < -0.99 ? -0.999 : raw; const trigger = fisher.value; fisher.value = 0.5 * Math.log((1 + value.value) / (1 - value.value)) + 0.5 * nz(fisher.value); fisherLine.set(fisher.value); triggerLine.set(trigger); }); ``` ### builtins/frama.ts ```ts // Fractal Adaptive Moving Average (John Ehlers, TASC October 2005): an EMA of hl2 whose alpha // follows the fractal dimension of the last `length` bars, measured from the ranges of their two // halves against the range of the whole: exp(−4.6 × (D − 1)), between 0.01 and 1. Smooth in // chop, quick in trends. For its first `length` bars it is the price. Reads any bar type. indicator('FRAMA', { overlay: true, shortName: 'FRAMA' }); const length = input.int('Length', 16, { min: 2, max: 500, step: 2 }); const source = input.source('Source', 'hl2'); const n = length % 2 === 0 ? length : length + 1; const half = n / 2; const chart = bars(); const highs = series(n); const lows = series(n); const filt = state.bar(na); const line = plot.line('FRAMA', { color: color.study(10) }); // The range of the highs and lows from `from` bars back, `count` bars long. function rangeOf(from: number, count: number) { let hi = -Infinity; let lo = Infinity; for (let k = from; k < from + count; k += 1) { hi = Math.max(hi, highs.ago(k)); lo = Math.min(lo, lows.ago(k)); } return hi - lo; } chart.onBar(() => { const x = chart.source(source); highs.set(chart.high); lows.set(chart.low); if (chart.index < n) { filt.value = x; line.set(x); return; } const n1 = rangeOf(0, half) / half; const n2 = rangeOf(half, half) / half; const n3 = rangeOf(0, n) / n; let alpha = 0.01; if (n1 > 0 && n2 > 0 && n3 > 0) { const dimension = (Math.log(n1 + n2) - Math.log(n3)) / Math.log(2); alpha = Math.min(1, Math.max(0.01, Math.exp(-4.6 * (dimension - 1)))); } filt.value = alpha * x + (1 - alpha) * filt.value; line.set(filt.value); }); ``` ### builtins/funding-rate.ts ```ts // Funding rate: what longs pay shorts (positive, up-coloured) or shorts pay longs (negative, // down-coloured) on each bar, in percent per hour as the venue pays it, per 8 hours as most // exchanges quote it, or a year's worth. From 0xArchive's one-minute funding readings: a bar's // rate is its last minute's, so a 1h bar holds the rate its hour paid and the forming bar shows // the rate the hour is heading for. Time charts of 1m or longer only: seconds, tick, volume and // range bars have none. indicator('Funding Rate', { shortName: 'Funding', format: 'percent' }); const period = input.choice('Period', '1h', ['1h', '8h', '1y']); const style = input.choice('Style', 'Columns', ['Columns', 'Line'], { legend: false }); const hours = period === '1y' ? 24 * 365 : period === '8h' ? 8 : 1; // Hourly rates are a few thousandths of a percent; a year's are whole percents. const precision = period === '1y' ? 2 : 4; const chart = bars(); const rate = funding(); const columns = plot.histogram('Funding', { color: color.up, precision, hidden: style !== 'Columns' }); const line = plot.line('Funding', { color: color.up, width: 2, precision, hidden: style !== 'Line' }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const value = rate.rate({ hours }) * 100; const side = value >= 0 ? color.up : color.down; if (style === 'Line') { line.set(value, side); return; } columns.set(value, side); }); ``` ### builtins/halftrend.ts ```ts // HalfTrend, as Alex Orekhov's (everget) indicator describes it: it turns down when the average of // the last `amplitude` highs falls below the highest low since it turned up and the close drops // below the previous low, and turns up the other way round; its line then holds the highest low // (up) or the lowest high (down) of the trend. Channel lines sit half the 100-bar ATR times the // deviation either side. Written from the description; no code copied. indicator('HalfTrend', { overlay: true, shortName: 'HalfTrend' }); const amplitude = input.int('Amplitude', 2, { min: 1, max: 100 }); const channelDeviation = input.float('Channel deviation', 2, { min: 0, step: 0.1 }); const chart = bars(); const atr = ta.atr(100); const highest = ta.highest(amplitude); const lowest = ta.lowest(amplitude); const highAverage = ta.sma(amplitude); const lowAverage = ta.sma(amplitude); const trend = state.bar(0); const nextTrend = state.bar(0); const maxLow = state.bar(na); const minHigh = state.bar(na); const up = state.bar(na); const down = state.bar(na); const line = plot.line('HalfTrend', { color: color.up, width: 2 }); const upper = plot.line('ATR high', { color: color.withAlpha(color.down, 0.5), style: 'dotted', axisLabel: false }); const lower = plot.line('ATR low', { color: color.withAlpha(color.up, 0.5), style: 'dotted', axisLabel: false }); chart.onBar(() => { const previous = chart.ago(1); const atr2 = atr.update(chart) / 2; const high = highest.update(chart.high); const low = lowest.update(chart.low); const highMa = highAverage.update(chart.high); const lowMa = lowAverage.update(chart.low); if (isNa(maxLow.value)) maxLow.value = nz(previous.low, chart.low); if (isNa(minHigh.value)) minHigh.value = nz(previous.high, chart.high); if (isNa(high) || isNa(low)) return; const lastTrend = trend.value; if (nextTrend.value === 1) { maxLow.value = Math.max(low, maxLow.value); if (highMa < maxLow.value && chart.close < nz(previous.low, chart.low)) { trend.value = 1; nextTrend.value = 0; minHigh.value = high; } } else { minHigh.value = Math.min(high, minHigh.value); if (lowMa > minHigh.value && chart.close > nz(previous.high, chart.high)) { trend.value = 0; nextTrend.value = 1; maxLow.value = low; } } if (trend.value === 0) up.value = lastTrend !== 0 ? nz(down.value, maxLow.value) : isNa(up.value) ? maxLow.value : Math.max(maxLow.value, up.value); else down.value = lastTrend !== 1 ? nz(up.value, minHigh.value) : isNa(down.value) ? minHigh.value : Math.min(minHigh.value, down.value); const value = trend.value === 0 ? up.value : down.value; const deviation = channelDeviation * atr2; line.set(value, trend.value === 0 ? color.up : color.down); upper.set(value + deviation); lower.set(value - deviation); }); ``` ### builtins/historical-volatility.ts ```ts // Historical Volatility: the standard deviation of the bar-to-bar log returns over `length` bars, // in percent, annualised over the bars in a 365-day year, as crypto trades every day and hour. // TradingView's built-in annualises intraday bars as if they were days; this one does not. Bars // that close on trades have no fixed length, so there it is per bar, not annualised. indicator('Historical Volatility', { shortName: 'HV' }); const length = input.int('Length', 10, { min: 2, max: 500 }); const chart = bars(); const stdev = ta.stdev(length); const scale = chart.interval.isTime ? Math.sqrt((365 * 86_400_000) / chart.interval.ms) : 1; const line = plot.line('HV', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { const previous = chart.ago(1).close; line.set(100 * stdev.update(Math.log(chart.close / previous)) * scale); }); ``` ### builtins/hma.ts ```ts // Hull moving average, as TradingView's built-in HMA: a weighted average over √length bars of // twice the half-length weighted average less the full-length one. Fast and smooth, at the cost // of overshooting turns. Reads any bar type. indicator('HMA', { overlay: true, shortName: 'HMA' }); const length = input.int('Length', 9, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const hma = ta.hma(length); const line = plot.line('HMA', { color: color.study(9) }); chart.onBar(() => { line.set(hma.update(chart.source(source))); }); ``` ### builtins/hull-suite.ts ```ts // Hull Suite, after InSilico's: a Hull moving average (HMA), its exponential form (EHMA) or its // triple-weighted form (THMA) of the close over 55 bars, coloured up while it is above its value two // bars back and down while below. Written from the description; no code copied. indicator('Hull Suite', { overlay: true, shortName: 'Hull' }); const mode = input.choice('Variation', 'HMA', ['HMA', 'EHMA', 'THMA']); const length = input.int('Length', 55, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const hma = ta.hma(length); const halfEma = ta.ema(Math.max(1, Math.floor(length / 2))); const fullEma = ta.ema(length); const ehma = ta.ema(Math.max(1, Math.round(Math.sqrt(length)))); const third = ta.wma(Math.max(1, Math.floor(length / 3))); const half = ta.wma(Math.max(1, Math.floor(length / 2))); const full = ta.wma(length); const thma = ta.wma(length); const history = series(2); const line = plot.line('Hull', { color: color.up, width: 2 }); chart.onBar(() => { const x = chart.source(source); // Every average advances on every bar, whichever is drawn. const h = hma.update(x); const e = ehma.update(2 * halfEma.update(x) - fullEma.update(x)); const t = thma.update(3 * third.update(x) - half.update(x) - full.update(x)); const value = mode === 'EHMA' ? e : mode === 'THMA' ? t : h; history.set(value); line.set(value, value > history.ago(2) ? color.up : color.down); }); ``` ### builtins/ichimoku.ts ```ts // Ichimoku Cloud, as TradingView's built-in: the conversion and base lines are the midpoints of // the highest high and lowest low over 9 and 26 bars; leading span A is their average and leading // span B the midpoint over 52 bars, both drawn 25 bars ahead (TradingView counts the displacement // of 26 from the current bar), with the cloud between them; the lagging span is the close drawn // 25 bars back. Reads any bar type: it needs the high, low and close. indicator('Ichimoku Cloud', { overlay: true, shortName: 'Ichimoku' }); const conversionLength = input.int('Conversion line length', 9, { min: 1, max: 500 }); const baseLength = input.int('Base line length', 26, { min: 1, max: 500 }); const spanBLength = input.int('Leading span B length', 52, { min: 1, max: 500 }); const displacement = input.int('Lagging span', 26, { min: 1, max: 500 }); const chart = bars(); const conversionHigh = ta.highest(conversionLength); const conversionLow = ta.lowest(conversionLength); const baseHigh = ta.highest(baseLength); const baseLow = ta.lowest(baseLength); const spanBHigh = ta.highest(spanBLength); const spanBLow = ta.lowest(spanBLength); const conversion = plot.line('Conversion line', { color: color.study(12) }); const base = plot.line('Base line', { color: color.study(6) }); const lagging = plot.line('Lagging span', { color: color.study(10), offset: 1 - displacement }); const spanA = plot.line('Leading span A', { color: color.withAlpha(color.up, 0.75), offset: displacement - 1 }); const spanB = plot.line('Leading span B', { color: color.withAlpha(color.down, 0.75), offset: displacement - 1 }); const cloud = plot.fill('Cloud', spanA, spanB); chart.onBar(() => { const c = (conversionHigh.update(chart.high) + conversionLow.update(chart.low)) / 2; const b = (baseHigh.update(chart.high) + baseLow.update(chart.low)) / 2; const a = (c + b) / 2; const s = (spanBHigh.update(chart.high) + spanBLow.update(chart.low)) / 2; conversion.set(c); base.set(b); lagging.set(chart.close); spanA.set(a); spanB.set(s); cloud.set(a > s ? color.withAlpha(color.up, 0.12) : color.withAlpha(color.down, 0.12)); }); ``` ### builtins/instantaneous-trendline.ts ```ts // Instantaneous Trendline (John Ehlers, Cybernetic Analysis, 2004): a low-lag trend filter of hl2 // with smoothing alpha, and its trigger, twice the trendline less its value two bars back. For the // first seven bars it is a 1-2-1 average of the price, as Ehlers' code starts. Reads any bar type. indicator('Ehlers Instantaneous Trendline', { overlay: true, shortName: 'iTrend' }); const alpha = input.float('Alpha', 0.07, { min: 0.01, max: 1, step: 0.01 }); const source = input.source('Source', 'hl2'); const chart = bars(); const price = series(2); const it = series(2); const trend = plot.line('Trendline', { color: color.study(12) }); const trigger = plot.line('Trigger', { color: color.study(8) }); chart.onBar(() => { const x = chart.source(source); price.set(x); if (chart.index < 7) it.set((x + 2 * nz(price.ago(1), x) + nz(price.ago(2), x)) / 4); else it.set((alpha - (alpha * alpha) / 4) * x + 0.5 * alpha * alpha * price.ago(1) - (alpha - 0.75 * alpha * alpha) * price.ago(2) + 2 * (1 - alpha) * it.ago(1) - (1 - alpha) ** 2 * it.ago(2)); trend.set(it.value); trigger.set(chart.index < 2 ? na : 2 * it.value - it.ago(2)); }); ``` ### builtins/kama.ts ```ts // Kaufman adaptive moving average (Perry Kaufman, Smarter Trading, 1995): an EMA whose speed // follows the efficiency ratio, the net move over `length` bars divided by the sum of every // bar's move. Trending, it runs at the fast EMA's speed; choppy, at the slow one's. The first // value is on bar `length`, stepping from the previous bar's source, as TA-Lib's KAMA does; a // window with no movement counts as fully efficient, also as TA-Lib does. indicator('KAMA', { overlay: true, shortName: 'KAMA' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const fastLength = input.int('Fast length', 2, { min: 1, max: 500 }); const slowLength = input.int('Slow length', 30, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const fast = 2 / (fastLength + 1); const slow = 2 / (slowLength + 1); const chart = bars(); const history = series(length); const noise = ta.sma(length); const kama = state.bar(na); const line = plot.line('KAMA', { color: color.study(0) }); chart.onBar(() => { const price = chart.source(source); history.set(price); const previous = history.ago(1); const moves = noise.update(Math.abs(price - previous)) * length; const net = Math.abs(price - history.ago(length)); if (isNa(moves) || isNa(net)) return; const efficiency = moves > net ? net / moves : 1; const sc = (efficiency * (fast - slow) + slow) ** 2; const from = isNa(kama.value) ? previous : kama.value; kama.value = from + sc * (price - from); line.set(kama.value); }); ``` ### builtins/keltner.ts ```ts // Keltner Channels, as TradingView's built-in: a moving average of the source (exponential by // default) and bands the multiplier times a range either side of it. The range is the average // true range over its own length (10 by default, Wilder's RMA), or the true range itself, or // the average of high less low. Not `ta.kc`, whose range is an EMA of the true range over the // same length as the average. On the price pane with the channel shaded. Reads any bar type: // it needs the high, low and close. indicator('Keltner Channels', { overlay: true, shortName: 'KC' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const multiplier = input.float('Multiplier', 2, { min: 0.001, step: 0.1 }); const source = input.source('Source', 'close'); const exponential = input.bool('Use exponential MA', true); const style = input.choice('Bands style', 'Average True Range', ['Average True Range', 'True Range', 'Range']); const atrLength = input.int('ATR length', 10, { min: 1, max: 500 }); const chart = bars(); const ema = ta.ema(length); const sma = ta.sma(length); const trueRange = ta.tr(true); const atr = ta.atr(atrLength); const rangeAverage = ta.rma(length); const basis = plot.line('Basis', { color: color.study(12) }); const upper = plot.line('Upper', { color: color.study(12) }); const lower = plot.line('Lower', { color: color.study(12) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.1) }); chart.onBar(() => { // Every average advances on every bar, whichever is drawn: an indicator only moves when it is updated. const price = chart.source(source); const e = ema.update(price); const s = sma.update(price); const tr = trueRange.update(chart); const a = atr.update(chart); const r = rangeAverage.update(chart.high - chart.low); const middle = exponential ? e : s; const range = style === 'True Range' ? tr : style === 'Average True Range' ? a : r; basis.set(middle); upper.set(middle + range * multiplier); lower.set(middle - range * multiplier); }); ``` ### builtins/klinger.ts ```ts // Klinger Oscillator (Stephen Klinger), as TradingView's built-in: volume signed by whether hlc3 // rose (or held) or fell, its 34-bar EMA less its 55-bar EMA, and a 13-bar EMA of that as the // signal. Needs volume. indicator('Klinger Oscillator', { shortName: 'KO', precision: 0 }); const chart = bars(); const change = ta.change(1); const fast = ta.ema(34); const slow = ta.ema(55); const signal = ta.ema(13); const line = plot.line('Klinger Oscillator', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(4) }); chart.onBar(() => { const signed = change.update(chart.source('hlc3')) >= 0 ? chart.volume : -chart.volume; const kvo = fast.update(signed) - slow.update(signed); line.set(kvo); signalLine.set(signal.update(kvo)); }); ``` ### builtins/kst.ts ```ts // Know Sure Thing (Martin Pring), as TradingView's built-in KST: four rates of change (10, 15, 20 // and 30 bars), each smoothed by a simple average (10, 10, 10 and 15), weighted 1 to 4 and summed; // the signal a 9-bar simple average of that. Reads any bar type. indicator('Know Sure Thing', { shortName: 'KST' }); const roc1 = input.int('RoC length #1', 10, { min: 1, max: 500 }); const roc2 = input.int('RoC length #2', 15, { min: 1, max: 500 }); const roc3 = input.int('RoC length #3', 20, { min: 1, max: 500 }); const roc4 = input.int('RoC length #4', 30, { min: 1, max: 500 }); const sma1 = input.int('SMA length #1', 10, { min: 1, max: 500 }); const sma2 = input.int('SMA length #2', 10, { min: 1, max: 500 }); const sma3 = input.int('SMA length #3', 10, { min: 1, max: 500 }); const sma4 = input.int('SMA length #4', 15, { min: 1, max: 500 }); const signalLength = input.int('Signal line length', 9, { min: 1, max: 500 }); const chart = bars(); const r1 = ta.roc(roc1); const r2 = ta.roc(roc2); const r3 = ta.roc(roc3); const r4 = ta.roc(roc4); const s1 = ta.sma(sma1); const s2 = ta.sma(sma2); const s3 = ta.sma(sma3); const s4 = ta.sma(sma4); const signal = ta.sma(signalLength); const kstLine = plot.line('KST', { color: color.study(10), margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(6) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const c = chart.close; const kst = s1.update(r1.update(c)) + 2 * s2.update(r2.update(c)) + 3 * s3.update(r3.update(c)) + 4 * s4.update(r4.update(c)); kstLine.set(kst); signalLine.set(signal.update(kst)); }); ``` ### builtins/laguerre-filter.ts ```ts // Laguerre Filter (John Ehlers, Time Warp, 2002): four all-pass Laguerre stages of hl2 with damping // gamma, weighted 1, 2, 2, 1. A larger gamma is smoother and slower. Every stage starts at the // first price instead of zero. Reads any bar type. indicator('Laguerre Filter', { overlay: true, shortName: 'Laguerre' }); const gamma = input.float('Gamma', 0.8, { min: 0, max: 0.99, step: 0.05 }); const source = input.source('Source', 'hl2'); const chart = bars(); const l0 = state.bar(na); const l1 = state.bar(na); const l2 = state.bar(na); const l3 = state.bar(na); const line = plot.line('Laguerre', { color: color.study(5) }); chart.onBar(() => { const x = chart.source(source); if (isNa(l0.value)) { l0.value = x; l1.value = x; l2.value = x; l3.value = x; } const p0 = l0.value, p1 = l1.value, p2 = l2.value; l0.value = (1 - gamma) * x + gamma * p0; l1.value = -gamma * l0.value + p0 + gamma * p1; l2.value = -gamma * l1.value + p1 + gamma * p2; l3.value = -gamma * l2.value + p2 + gamma * l3.value; line.set((l0.value + 2 * l1.value + 2 * l2.value + l3.value) / 6); }); ``` ### builtins/laguerre-rsi.ts ```ts // Laguerre RSI (John Ehlers, Time Warp, 2002): an RSI of the four stages of a Laguerre filter of // the close instead of its bar-to-bar changes, from 0 to 100, quick with little lag. Gamma sets // how far back it looks. Bands at 80 and 20. Every stage starts at the first price. Reads any bar // type. indicator('Laguerre RSI', { shortName: 'LRSI' }); const gamma = input.float('Gamma', 0.5, { min: 0, max: 0.99, step: 0.05 }); const source = input.source('Source', 'close'); const chart = bars(); const l0 = state.bar(na); const l1 = state.bar(na); const l2 = state.bar(na); const l3 = state.bar(na); const line = plot.line('Laguerre RSI', { color: color.study(13), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const lower = plot.level('Lower band', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(13), 0.08) }); chart.onBar(() => { const x = chart.source(source); if (isNa(l0.value)) { l0.value = x; l1.value = x; l2.value = x; l3.value = x; } const p0 = l0.value, p1 = l1.value, p2 = l2.value; l0.value = (1 - gamma) * x + gamma * p0; l1.value = -gamma * l0.value + p0 + gamma * p1; l2.value = -gamma * l1.value + p1 + gamma * p2; l3.value = -gamma * l2.value + p2 + gamma * l3.value; let up = 0; let down = 0; for (const [a, b] of [[l0.value, l1.value], [l1.value, l2.value], [l2.value, l3.value]]) { if (a >= b) up += a - b; else down += b - a; } line.set(up + down === 0 ? 0 : (100 * up) / (up + down)); }); ``` ### builtins/linreg-channel.ts ```ts // Linear Regression Channel, as TradingView's built-in: the least-squares line through the source // over the last `length` bars, and lines the deviation times the standard deviation of the // residuals (divided by length − 1, as TradingView's) above and below it. Drawn over those bars // on the newest bar only, so it moves as bars arrive. Reads any bar type. indicator('Linear Regression Channel', { overlay: true, shortName: 'LinReg', maxLines: 3, maxLinefills: 2 }); const length = input.int('Length', 100, { min: 2, max: 5000 }); const source = input.source('Source', 'close'); const upperDeviation = input.float('Upper deviation', 2, { min: 0, step: 0.1 }); const lowerDeviation = input.float('Lower deviation', 2, { min: 0, step: 0.1 }); const chart = bars(); const history = series(length); const tint = color.study(12); chart.onBar(() => { history.set(chart.source(source)); if (!chart.isLast || chart.index < length - 1) return; // x runs 0 (oldest) to length − 1 (this bar). let sx = 0, sy = 0, sxx = 0, sxy = 0; for (let x = 0; x < length; x += 1) { const y = history.ago(length - 1 - x); if (isNa(y)) return; sx += x; sy += y; sxx += x * x; sxy += x * y; } const slope = (length * sxy - sx * sy) / (length * sxx - sx * sx); const intercept = (sy - slope * sx) / length; let squares = 0; for (let x = 0; x < length; x += 1) { const residual = history.ago(length - 1 - x) - (intercept + slope * x); squares += residual * residual; } const deviation = Math.sqrt(squares / (length - 1)); const from = chart.index - (length - 1); const start = intercept; const end = intercept + slope * (length - 1); const base = draw.line({ from: { bar: from, price: start }, to: { bar: chart.index, price: end }, color: tint, width: 2 }); const upper = draw.line({ from: { bar: from, price: start + upperDeviation * deviation }, to: { bar: chart.index, price: end + upperDeviation * deviation }, color: color.withAlpha(tint, 0.6) }); const lower = draw.line({ from: { bar: from, price: start - lowerDeviation * deviation }, to: { bar: chart.index, price: end - lowerDeviation * deviation }, color: color.withAlpha(tint, 0.6) }); draw.linefill(upper, base, { color: color.withAlpha(tint, 0.08) }); draw.linefill(base, lower, { color: color.withAlpha(tint, 0.08) }); }); ``` ### builtins/linreg-slope.ts ```ts // Linear Regression Slope: the slope of the least-squares line through the source's last `length` // values, in price per bar (ta.linreg's end less its value one bar back). Reads any bar type. indicator('Linear Regression Slope', { shortName: 'LRS' }); const length = input.int('Length', 14, { min: 2, max: 500 }); const source = input.source('Source', 'close'); // In price units but small beside the price: the market's decimals and two more. function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const end = ta.linreg(length, 0); const before = ta.linreg(length, 1); const line = plot.line('Slope', { color: color.study(12), precision: decimalsOf(chart.market.tickSize), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const price = chart.source(source); line.set(end.update(price) - before.update(price)); }); ``` ### builtins/lsma.ts ```ts // Least squares moving average, as TradingView's built-in LSMA: the end of the straight line fitted // to the last `length` values (`ta.linreg`). The offset reads the fitted line that many bars back // from its end, and does not shift the plot. Reads any bar type. indicator('LSMA', { overlay: true, shortName: 'LSMA' }); const length = input.int('Length', 25, { min: 1, max: 500 }); const offset = input.int('Offset', 0, { min: 0, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const linreg = ta.linreg(length, offset); const line = plot.line('LSMA', { color: color.study(11) }); chart.onBar(() => { line.set(linreg.update(chart.source(source))); }); ``` ### builtins/ma-cross.ts ```ts // MA Cross, as TradingView's built-in: a short and a long moving average (simple by default), and // a cross drawn on the short one on every bar where the two cross. Reads any bar type; the // volume-weighted average needs volume. indicator('MA Cross', { overlay: true, shortName: 'MA Cross' }); const shortLength = input.int('Short length', 9, { min: 1, max: 500 }); const longLength = input.int('Long length', 21, { min: 1, max: 500 }); const kind = input.choice('Type', 'SMA', ['SMA', 'EMA', 'SMMA', 'WMA', 'VWMA']); const source = input.source('Source', 'close'); // The average, chosen once when the script loads. Each takes the value and the bar's volume. function average(n: number) { if (kind === 'VWMA') { const a = ta.vwma(n); return (x: number, v: number) => a.update(x, v); } const a = kind === 'EMA' ? ta.ema(n) : kind === 'SMMA' ? ta.rma(n) : kind === 'WMA' ? ta.wma(n) : ta.sma(n); return (x: number, _v: number) => a.update(x); } const chart = bars(); const fast = average(shortLength); const slow = average(longLength); const crossing = ta.cross(); const shortLine = plot.line('Short', { color: color.study(12) }); const longLine = plot.line('Long', { color: color.study(8) }); const crosses = plot.points('Cross', { shape: 'cross', color: color.study(9) }); chart.onBar(() => { const price = chart.source(source); const a = fast(price, chart.volume); const b = slow(price, chart.volume); shortLine.set(a); longLine.set(b); if (crossing.update(a, b)) crosses.set(a); }); ``` ### builtins/ma-ribbon.ts ```ts // Moving Average Ribbon, as TradingView's built-in: four moving averages of one source, 20, 50, // 100 and 200 bars long and simple by default, each its own type and length. Reads any bar type; // the volume-weighted average needs volume. indicator('MA Ribbon', { overlay: true, shortName: 'Ribbon' }); const KINDS = ['SMA', 'EMA', 'SMMA', 'WMA', 'VWMA']; const kind1 = input.choice('MA 1 type', 'SMA', KINDS, { group: 'MA 1' }); const length1 = input.int('MA 1 length', 20, { min: 1, max: 500, group: 'MA 1', inline: true }); const kind2 = input.choice('MA 2 type', 'SMA', KINDS, { group: 'MA 2' }); const length2 = input.int('MA 2 length', 50, { min: 1, max: 500, group: 'MA 2', inline: true }); const kind3 = input.choice('MA 3 type', 'SMA', KINDS, { group: 'MA 3' }); const length3 = input.int('MA 3 length', 100, { min: 1, max: 500, group: 'MA 3', inline: true }); const kind4 = input.choice('MA 4 type', 'SMA', KINDS, { group: 'MA 4' }); const length4 = input.int('MA 4 length', 200, { min: 1, max: 500, group: 'MA 4', inline: true }); const source = input.source('Source', 'close'); // The average, chosen once when the script loads. Each takes the value and the bar's volume. function average(kind: string, n: number) { if (kind === 'VWMA') { const a = ta.vwma(n); return (x: number, v: number) => a.update(x, v); } const a = kind === 'EMA' ? ta.ema(n) : kind === 'SMMA' ? ta.rma(n) : kind === 'WMA' ? ta.wma(n) : ta.sma(n); return (x: number, _v: number) => a.update(x); } const chart = bars(); const ma1 = average(kind1, length1); const ma2 = average(kind2, length2); const ma3 = average(kind3, length3); const ma4 = average(kind4, length4); const line1 = plot.line('MA 1', { color: color.study(9) }); const line2 = plot.line('MA 2', { color: color.study(8) }); const line3 = plot.line('MA 3', { color: color.study(6) }); const line4 = plot.line('MA 4', { color: color.study(13) }); chart.onBar(() => { const price = chart.source(source); line1.set(ma1(price, chart.volume)); line2.set(ma2(price, chart.volume)); line3.set(ma3(price, chart.volume)); line4.set(ma4(price, chart.volume)); }); ``` ### builtins/macd.ts ```ts // Moving average convergence divergence, as TradingView's built-in MACD: the fast EMA less the // slow EMA, its signal an EMA of that, and the histogram the difference. Every EMA is seeded with // the simple average of its first values, as `ta.ema` is, so the MACD line starts on bar // `slow - 1` and the signal `signal - 1` bars after it. The histogram is coloured by side of zero // and by whether it grew, as TradingView's is. Reads any bar type. indicator('MACD', { shortName: 'MACD' }); const fastLength = input.int('Fast length', 12, { min: 1, max: 500 }); const slowLength = input.int('Slow length', 26, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const signalLength = input.int('Signal smoothing', 9, { min: 1, max: 500 }); // Values are in the market's price units but small beside the price, so they print with the // market's decimals and two more (BTC at a tick of 1 prints -26.31, as it does on a 0.01 tick). function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const macd = ta.macd(fastLength, slowLength, signalLength); const previous = state.bar(na); const precision = decimalsOf(chart.market.tickSize); // The histogram keeps its value in the legend and off the axis: three tags in a short pane cover one another. const histogram = plot.histogram('Histogram', { precision, axisLabel: false }); const macdLine = plot.line('MACD', { color: color.study(12), precision, margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(8), precision }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const value = macd.update(chart.source(source)); const h = value.histogram; // Above zero the columns are strong while they grow and light while they shrink; below zero the other way round. const grew = previous.value < h; histogram.set(h, h >= 0 ? (grew ? color.up : color.withAlpha(color.up, 0.45)) : (grew ? color.withAlpha(color.down, 0.45) : color.down)); macdLine.set(value.macd); signalLine.set(value.signal); previous.value = h; }); ``` ### builtins/mama.ts ```ts // MESA Adaptive Moving Average (John Ehlers, TASC September 2001): MAMA and its following average // FAMA. A Hilbert transform measures the phase of the price's dominant cycle; the faster the // phase turns, the closer alpha moves to the fast limit, and the slower, to the slow limit. As // Ehlers' EasyLanguage, with arctangents in degrees and every series starting at zero, the // transform runs from the sixth bar; unlike it, MAMA and FAMA start at that bar's price instead // of climbing from zero, and are drawn from bar 32, TA-Lib's warm-up. Reads any bar type. indicator('MESA Adaptive Moving Average', { overlay: true, shortName: 'MAMA' }); const fastLimit = input.float('Fast limit', 0.5, { min: 0.01, max: 1, step: 0.05 }); const slowLimit = input.float('Slow limit', 0.05, { min: 0.01, max: 1, step: 0.01 }); const source = input.source('Source', 'hl2'); const DEG = 180 / Math.PI; const chart = bars(); const price = series(3); const smooth = series(6); const detrender = series(6); const i1 = series(6); const q1 = series(6); const i2 = state.bar(0); const q2 = state.bar(0); const re = state.bar(0); const im = state.bar(0); const period = state.bar(0); const phase = state.bar(0); const mama = state.bar(0); const fama = state.bar(0); const mamaLine = plot.line('MAMA', { color: color.study(12), width: 2 }); const famaLine = plot.line('FAMA', { color: color.study(8) }); // Ehlers' Hilbert transform kernel over a series' last seven values. function hilbert(s: Series, gain: number) { return (0.0962 * s.value + 0.5769 * nz(s.ago(2)) - 0.5769 * nz(s.ago(4)) - 0.0962 * nz(s.ago(6))) * gain; } chart.onBar(() => { const x = chart.source(source); price.set(x); if (chart.index < 5) { smooth.set(0); detrender.set(0); i1.set(0); q1.set(0); return; } const gain = 0.075 * period.value + 0.54; smooth.set((4 * x + 3 * price.ago(1) + 2 * price.ago(2) + price.ago(3)) / 10); detrender.set(hilbert(smooth, gain)); q1.set(hilbert(detrender, gain)); i1.set(nz(detrender.ago(3))); const jI = hilbert(i1, gain); const jQ = hilbert(q1, gain); const lastI2 = i2.value; const lastQ2 = q2.value; i2.value = 0.2 * (i1.value - jQ) + 0.8 * lastI2; q2.value = 0.2 * (q1.value + jI) + 0.8 * lastQ2; re.value = 0.2 * (i2.value * lastI2 + q2.value * lastQ2) + 0.8 * re.value; im.value = 0.2 * (i2.value * lastQ2 - q2.value * lastI2) + 0.8 * im.value; const before = period.value; let p = before; if (im.value !== 0 && re.value !== 0) p = 360 / (Math.atan(im.value / re.value) * DEG); if (p > 1.5 * before) p = 1.5 * before; if (p < 0.67 * before) p = 0.67 * before; if (p < 6) p = 6; if (p > 50) p = 50; period.value = 0.2 * p + 0.8 * before; const lastPhase = phase.value; if (i1.value !== 0) phase.value = Math.atan(q1.value / i1.value) * DEG; let delta = lastPhase - phase.value; if (delta < 1) delta = 1; let alpha = fastLimit / delta; if (alpha < slowLimit) alpha = slowLimit; const lastMama = chart.index === 5 ? x : mama.value; mama.value = alpha * x + (1 - alpha) * lastMama; const lastFama = chart.index === 5 ? x : fama.value; fama.value = 0.5 * alpha * mama.value + (1 - 0.5 * alpha) * lastFama; if (chart.index < 32) return; mamaLine.set(mama.value); famaLine.set(fama.value); }); ``` ### builtins/mass-index.ts ```ts // Mass Index (Donald Dorsey, 1992), as TradingView's built-in: the 9-bar EMA of the bar's range // over the 9-bar EMA of that EMA, summed over `length` bars. A "reversal bulge" rises past 27 and // falls back below 26.5. Reads any bar type: it needs the high and low. indicator('Mass Index', { shortName: 'Mass' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const chart = bars(); const single = ta.ema(9); const double = ta.ema(9); const window = ta.sma(length); const line = plot.line('Mass Index', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { const e = single.update(chart.high - chart.low); line.set(window.update(e / double.update(e)) * length); }); ``` ### builtins/mcginley.ts ```ts // McGinley Dynamic (John McGinley, 1990s), as TradingView's built-in: it starts at the EMA over // `length` (seeded like `ta.ema`), then moves by (value − previous) / (length × (value / previous)^4), // slowing as price runs away from it and speeding up as price falls back. Reads any bar type. indicator('McGinley Dynamic', { overlay: true, shortName: 'MGD' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const ema = ta.ema(length); const dynamic = state.bar(na); const line = plot.line('McGinley Dynamic', { color: color.study(5) }); chart.onBar(() => { const price = chart.source(source); const seed = ema.update(price); const previous = dynamic.value; dynamic.value = isNa(previous) ? seed : previous + (price - previous) / (length * (price / previous) ** 4); line.set(dynamic.value); }); ``` ### builtins/median.ts ```ts // Median: the middle value of the source over the last `length` bars (the mean of the two middle // ones for an even length), on the price pane. Less pulled by spikes than an average. Reads any // bar type. indicator('Median', { overlay: true, shortName: 'Median' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const history = series(length); const line = plot.line('Median', { color: color.study(14) }); chart.onBar(() => { history.set(chart.source(source)); if (chart.index < length - 1) return; const window: number[] = []; for (let k = 0; k < length; k += 1) { const v = history.ago(k); if (isNa(v)) return; window.push(v); } window.sort((a, b) => a - b); const middle = Math.floor(length / 2); line.set(length % 2 === 1 ? window[middle] : (window[middle - 1] + window[middle]) / 2); }); ``` ### builtins/mfi.ts ```ts // Money Flow Index (Gene Quong and Avrum Soudack), as TradingView's built-in MFI: an RSI of money // flow, hlc3 times volume, split by whether hlc3 rose or fell, over `length` bars. Bands at 80 and // 20. Needs volume. indicator('Money Flow Index', { shortName: 'MFI' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const chart = bars(); const mfi = ta.mfi(length); const line = plot.line('MF', { color: color.study(13), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Overbought', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Oversold', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(13), 0.08) }); chart.onBar(() => { line.set(mfi.update(chart)); }); ``` ### builtins/momentum.ts ```ts // Momentum, as TradingView's built-in: the source less the source `length` bars ago, in price // units. Reads any bar type. indicator('Momentum', { shortName: 'Mom' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const source = input.source('Source', 'close'); // Values are in the market's price units but small beside the price, so they print with the // market's decimals and two more. function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const mom = ta.mom(length); const line = plot.line('Momentum', { color: color.study(12), precision: decimalsOf(chart.market.tickSize), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(mom.update(chart.source(source))); }); ``` ### builtins/net-volume.ts ```ts // Net Volume, as TradingView's built-in: the bar's volume, positive when the close rose, negative // when it fell, zero when it held (and on the first loaded bar), as columns. Needs volume. indicator('Net Volume', { shortName: 'Net Volume', precision: 0 }); const chart = bars(); const change = ta.change(1); const columns = plot.histogram('Net Volume', { color: color.up, axisLabel: true }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const c = change.update(chart.close); const net = c > 0 ? chart.volume : c < 0 ? -chart.volume : 0 * chart.volume; columns.set(net, net >= 0 ? color.withAlpha(color.up, 0.6) : color.withAlpha(color.down, 0.6)); }); ``` ### builtins/obv.ts ```ts // On Balance Volume (Joseph Granville, 1963), as TradingView's built-in OBV: the running total of // volume, added on bars that close up and taken away on bars that close down, from the second // loaded bar. Its level depends on how much history is loaded; its slope does not. indicator('On Balance Volume', { shortName: 'OBV', precision: 0 }); const chart = bars(); const obv = ta.obv(); const line = plot.line('OBV', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { line.set(obv.update(chart)); }); ``` ### builtins/oi-delta.ts ```ts // Open interest delta: how much open interest each bar added or removed, close less open, so a bar // counts every minute inside it. Up-coloured where positions were opened, down-coloured where they // were closed. From 0xArchive's one-minute open interest, in the market's coins or in USD (the // change in coins at the bar's closing mark, so a price move alone adds nothing). On the newest bars, // and every bar on seconds, tick, volume and range charts, it is the sum of the bar's trades' own // changes in open interest, live with every trade. indicator('OI Delta', { shortName: 'ΔOI' }); const unit = input.choice('Unit', 'USD', ['USD', 'Coins']); const usd = unit === 'USD'; const chart = bars(); const oi = openInterest(); const delta = plot.histogram('OI delta', { color: color.up }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const change = oi.change({ usd }); delta.set(change, change >= 0 ? color.up : color.down); }); ``` ### builtins/open-interest.ts ```ts // Open interest: the market's open positions, as candles or a line. Each bar is built from // 0xArchive's one-minute open interest: it opens at the close of the minute before the bar, its // high and low are over the bar's minutes, and it closes at the bar's last minute. In USD (each // minute's open interest times its mark) or in the market's coins. The newest bars, and every bar on // seconds, tick, volume and range charts, are built from the trades instead: each trade's change in // open interest, anchored to the one-minute figure, so the forming bar moves with every trade. indicator('Open Interest', { shortName: 'OI' }); const style = input.choice('Style', 'Candles', ['Candles', 'Line'], { legend: false }); const unit = input.choice('Unit', 'USD', ['USD', 'Coins']); const usd = unit === 'USD'; const chart = bars(); const oi = openInterest(); const candles = plot.candles('Open interest', { color: color.up, hidden: style !== 'Candles', margins: { top: 0.1, bottom: 0.1 } }); const line = plot.line('Open interest', { color: color.study(12), width: 2, hidden: style !== 'Line', join: true, margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { const close = oi.close({ usd }); if (style === 'Line') { line.set(close); return; } const open = oi.open({ usd }); candles.set({ open, high: oi.high({ usd }), low: oi.low({ usd }), close }, close >= open ? color.up : color.down); }); ``` ### builtins/opening-range.ts ```ts // Opening Range: the high and low of the bars that open in the first `minutes` of a session, // drawn from the session's open until the next one, with the range shaded. On bars longer than the // range, that is the session's first bar. Sessions open at 00:00 UTC, or at the New York, London or // Tokyo cash open in that city's time (summer time included). Time bars shorter than a day only. indicator('Opening Range', { overlay: true, shortName: 'OR' }); const minutes = input.int('Minutes', 30, { min: 1, max: 720 }); const session = input.choice('Session', 'UTC 00:00', ['UTC 00:00', 'New York 09:30', 'London 08:00', 'Tokyo 09:00']); const chart = bars(); const zone = session === 'New York 09:30' ? 'America/New_York' : session === 'London 08:00' ? 'Europe/London' : session === 'Tokyo 09:00' ? 'Asia/Tokyo' : 'UTC'; const openMs = session === 'New York 09:30' ? 34_200_000 : session === 'London 08:00' ? 28_800_000 : session === 'Tokyo 09:00' ? 32_400_000 : 0; const usable = chart.interval.isTime && chart.interval.ms < 86_400_000; const start = state.bar(na); const high = state.bar(na); const low = state.bar(na); const upper = plot.line('OR high', { color: color.up }); const lower = plot.line('OR low', { color: color.down }); const shade = plot.fill('OR', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); /** The open of the session that contains `time`: today's open in the city, or yesterday's before it. */ function sessionOf(time: number) { const today = clock.startOf(time, 'day', zone) + openMs; return time >= today ? today : clock.startOf(time - 86_400_000, 'day', zone) + openMs; } chart.onBar(() => { if (!usable) return; const s = sessionOf(chart.time); if (s !== start.value) { start.value = s; high.value = na; low.value = na; } if (chart.time < s + minutes * 60_000) { high.value = isNa(high.value) ? chart.high : Math.max(high.value, chart.high); low.value = isNa(low.value) ? chart.low : Math.min(low.value, chart.low); } // The last bar before the next session draws nothing onward. const ends = sessionOf(chart.closeTime) !== s ? color.none : null; upper.set(high.value, ends); lower.set(low.value, ends); }); ``` ### builtins/parabolic-sar.ts ```ts // Parabolic SAR (J. Welles Wilder, 1978), as TradingView's built-in (`ta.sar`): a stop that trails // price and speeds up, by `increment` each time the trend makes a new extreme, up to `maximum`, // and flips to the other side when price reaches it. Drawn as crosses. The trend starts on the // second bar, up if it closed above the first. Reads any bar type: it needs the high, low and close. indicator('Parabolic SAR', { overlay: true, shortName: 'SAR' }); const start = input.float('Start', 0.02, { min: 0.001, step: 0.01 }); const increment = input.float('Increment', 0.02, { min: 0.001, step: 0.01 }); const maximum = input.float('Maximum', 0.2, { min: 0.001, step: 0.01 }); const chart = bars(); const sar = state.bar(na); const extreme = state.bar(na); const acceleration = state.bar(na); const below = state.bar(false); const dots = plot.points('SAR', { shape: 'cross', color: color.study(12) }); chart.onBar(() => { if (chart.index === 0) return; const prev = chart.ago(1); let first = false; if (chart.index === 1) { below.value = chart.close > prev.close; extreme.value = below.value ? chart.high : chart.low; sar.value = below.value ? prev.low : prev.high; first = true; acceleration.value = start; } let result = sar.value + acceleration.value * (extreme.value - sar.value); if (below.value) { if (result > chart.low) { first = true; below.value = false; result = Math.max(chart.high, extreme.value); extreme.value = chart.low; acceleration.value = start; } } else if (result < chart.high) { first = true; below.value = true; result = Math.min(chart.low, extreme.value); extreme.value = chart.high; acceleration.value = start; } if (!first) { if (below.value) { if (chart.high > extreme.value) { extreme.value = chart.high; acceleration.value = Math.min(acceleration.value + increment, maximum); } } else if (chart.low < extreme.value) { extreme.value = chart.low; acceleration.value = Math.min(acceleration.value + increment, maximum); } } if (below.value) { result = Math.min(result, prev.low); if (chart.index > 1) result = Math.min(result, chart.ago(2).low); } else { result = Math.max(result, prev.high); if (chart.index > 1) result = Math.max(result, chart.ago(2).high); } sar.value = result; dots.set(result); }); ``` ### builtins/percent-rank.ts ```ts // Percent Rank: the share of the previous `length` values of the source that are at or below this // one, from 0 to 100 (ta.percentrank). Reads any bar type. indicator('Percent Rank', { shortName: 'PR' }); const length = input.int('Length', 100, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const rank = ta.percentRank(length); const line = plot.line('Percent Rank', { color: color.study(13), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Upper', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.4) }); plot.level('Lower', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.4) }); chart.onBar(() => { line.set(rank.update(chart.source(source))); }); ``` ### builtins/pivot-points.ts ```ts // Pivot Points Standard, as TradingView's built-in: from the previous period's high, low and close // (and the current period's open, for Woodie), a pivot and support and resistance levels, drawn // across the current period and broken where the next begins. Traditional, Fibonacci, Woodie, // Classic, DM (DeMark, which has only R1 and S1) and Camarilla. The period is the UTC day, week or // month; Auto picks the day up to 15-minute bars, the week below daily bars, and the month above. // This draws R1 to R3 and S1 to S3; TradingView's Traditional also has R4, R5, S4 and S5. indicator('Pivot Points Standard', { overlay: true, shortName: 'Pivots' }); const kind = input.choice('Type', 'Traditional', ['Traditional', 'Fibonacci', 'Woodie', 'Classic', 'DM', 'Camarilla']); const timeframe = input.choice('Pivots timeframe', 'Auto', ['Auto', 'Daily', 'Weekly', 'Monthly']); const chart = bars(); const ms = chart.interval.isTime ? chart.interval.ms : 3_600_000; const unit = timeframe === 'Daily' ? 'day' : timeframe === 'Weekly' ? 'week' : timeframe === 'Monthly' ? 'month' : ms <= 900_000 ? 'day' : ms < 86_400_000 ? 'week' : 'month'; const period = state.bar(na); const open = state.bar(na); const high = state.bar(na); const low = state.bar(na); const close = state.bar(na); const lastHigh = state.bar(na); const lastLow = state.bar(na); const lastClose = state.bar(na); const lastOpen = state.bar(na); const pivot = plot.line('P', { color: color.study(9) }); const r1 = plot.line('R1', { color: color.withAlpha(color.down, 0.8) }); const s1 = plot.line('S1', { color: color.withAlpha(color.up, 0.8) }); const r2 = plot.line('R2', { color: color.withAlpha(color.down, 0.6) }); const s2 = plot.line('S2', { color: color.withAlpha(color.up, 0.6) }); const r3 = plot.line('R3', { color: color.withAlpha(color.down, 0.4) }); const s3 = plot.line('S3', { color: color.withAlpha(color.up, 0.4) }); chart.onBar(() => { const start = clock.startOf(chart.time, unit); if (start !== period.value) { if (!isNa(period.value)) { lastHigh.value = high.value; lastLow.value = low.value; lastClose.value = close.value; lastOpen.value = open.value; } period.value = start; open.value = chart.open; high.value = chart.high; low.value = chart.low; } else { high.value = Math.max(high.value, chart.high); low.value = Math.min(low.value, chart.low); } close.value = chart.close; const H = lastHigh.value; const L = lastLow.value; const C = lastClose.value; if (isNa(H)) return; const range = H - L; // A point's colour paints the segment that starts at it: the last bar of a period draws nothing onward. const ends = clock.startOf(chart.closeTime, unit) === chart.closeTime ? color.none : null; let p = (H + L + C) / 3; let levels: number[]; if (kind === 'Fibonacci') levels = [p + 0.382 * range, p - 0.382 * range, p + 0.618 * range, p - 0.618 * range, p + range, p - range]; else if (kind === 'Woodie') { p = (H + L + 2 * open.value) / 4; levels = [2 * p - L, 2 * p - H, p + range, p - range, H + 2 * (p - L), L - 2 * (H - p)]; } else if (kind === 'Classic') levels = [2 * p - L, 2 * p - H, p + range, p - range, p + 2 * range, p - 2 * range]; else if (kind === 'DM') { const O = lastOpen.value; const x = C < O ? H + 2 * L + C : C > O ? 2 * H + L + C : H + L + 2 * C; p = x / 4; levels = [x / 2 - L, x / 2 - H, na, na, na, na]; } else if (kind === 'Camarilla') levels = [C + (range * 1.1) / 12, C - (range * 1.1) / 12, C + (range * 1.1) / 6, C - (range * 1.1) / 6, C + (range * 1.1) / 4, C - (range * 1.1) / 4]; else levels = [2 * p - L, 2 * p - H, p + range, p - range, H + 2 * (p - L), L - 2 * (H - p)]; pivot.set(p, ends); r1.set(levels[0], ends); s1.set(levels[1], ends); r2.set(levels[2], ends); s2.set(levels[3], ends); r3.set(levels[4], ends); s3.set(levels[5], ends); }); ``` ### builtins/pivots-high-low.ts ```ts // Pivot Points High Low, as TradingView's built-in: a label with the price at each swing high (a // high above the `left` bars before it and the `right` bars after it) and each swing low. A swing // is known `right` bars after it, and is drawn back on its own bar. Reads any bar type. indicator('Pivot Points High Low', { overlay: true, shortName: 'Pivots HL' }); const left = input.int('Left bars', 10, { min: 1, max: 250 }); const right = input.int('Right bars', 10, { min: 1, max: 250 }); const chart = bars(); const highs = ta.pivotHigh(left, right); const lows = ta.pivotLow(left, right); const highLabels = plot.shape('Pivot high', { shape: 'labelDown', location: 'absolute', color: color.down, offset: -right }); const lowLabels = plot.shape('Pivot low', { shape: 'labelUp', location: 'absolute', color: color.up, offset: -right }); chart.onBar(() => { const h = highs.update(chart.high); const l = lows.update(chart.low); highLabels.set(!isNa(h), { price: h, text: fmt.price(h) }); lowLabels.set(!isNa(l), { price: l, text: fmt.price(l) }); }); ``` ### builtins/previous-high-low.ts ```ts // Previous day, week and month high and low: the extremes of the last complete UTC day (and week, // Monday to Sunday, and month), drawn across the current one. A period's lines show only on bars // shorter than it. Reads any bar type. indicator('Previous High and Low', { overlay: true, shortName: 'Prev H/L' }); const showDay = input.bool('Previous day', true); const showWeek = input.bool('Previous week', true); const showMonth = input.bool('Previous month', false); const chart = bars(); const ms = chart.interval.isTime ? chart.interval.ms : 0; const dayStart = state.bar(na); const dayHigh = state.bar(na); const dayLow = state.bar(na); const prevDayHigh = state.bar(na); const prevDayLow = state.bar(na); const weekStart = state.bar(na); const weekHigh = state.bar(na); const weekLow = state.bar(na); const prevWeekHigh = state.bar(na); const prevWeekLow = state.bar(na); const monthStart = state.bar(na); const monthHigh = state.bar(na); const monthLow = state.bar(na); const prevMonthHigh = state.bar(na); const prevMonthLow = state.bar(na); const pdh = plot.line('PDH', { color: color.down, style: 'dashed' }); const pdl = plot.line('PDL', { color: color.up, style: 'dashed' }); const pwh = plot.line('PWH', { color: color.withAlpha(color.down, 0.8) }); const pwl = plot.line('PWL', { color: color.withAlpha(color.up, 0.8) }); const pmh = plot.line('PMH', { color: color.withAlpha(color.down, 0.8), width: 2 }); const pml = plot.line('PML', { color: color.withAlpha(color.up, 0.8), width: 2 }); chart.onBar(() => { const d = clock.startOf(chart.time, 'day'); if (d !== dayStart.value) { if (!isNa(dayStart.value)) { prevDayHigh.value = dayHigh.value; prevDayLow.value = dayLow.value; } dayStart.value = d; dayHigh.value = chart.high; dayLow.value = chart.low; } else { dayHigh.value = Math.max(dayHigh.value, chart.high); dayLow.value = Math.min(dayLow.value, chart.low); } const w = clock.startOf(chart.time, 'week'); if (w !== weekStart.value) { if (!isNa(weekStart.value)) { prevWeekHigh.value = weekHigh.value; prevWeekLow.value = weekLow.value; } weekStart.value = w; weekHigh.value = chart.high; weekLow.value = chart.low; } else { weekHigh.value = Math.max(weekHigh.value, chart.high); weekLow.value = Math.min(weekLow.value, chart.low); } const m = clock.startOf(chart.time, 'month'); if (m !== monthStart.value) { if (!isNa(monthStart.value)) { prevMonthHigh.value = monthHigh.value; prevMonthLow.value = monthLow.value; } monthStart.value = m; monthHigh.value = chart.high; monthLow.value = chart.low; } else { monthHigh.value = Math.max(monthHigh.value, chart.high); monthLow.value = Math.min(monthLow.value, chart.low); } // The last bar of a period draws nothing onward, so each line breaks where the next period starts. const endOf = (unit: 'day' | 'week' | 'month') => (clock.startOf(chart.closeTime, unit) === chart.closeTime ? color.none : null); const shorter = (span: number) => ms > 0 && ms < span; if (showDay && shorter(86_400_000)) { pdh.set(prevDayHigh.value, endOf('day')); pdl.set(prevDayLow.value, endOf('day')); } if (showWeek && shorter(7 * 86_400_000)) { pwh.set(prevWeekHigh.value, endOf('week')); pwl.set(prevWeekLow.value, endOf('week')); } if (showMonth && shorter(28 * 86_400_000)) { pmh.set(prevMonthHigh.value, endOf('month')); pml.set(prevMonthLow.value, endOf('month')); } }); ``` ### builtins/price-oscillator.ts ```ts // Price Oscillator, as TradingView's built-in (the percentage price oscillator): the short moving // average less the long one, as a percentage of the long one. Simple averages by default, or // exponential. Reads any bar type. indicator('Price Oscillator', { shortName: 'PPO' }); const shortLength = input.int('Short length', 10, { min: 1, max: 500 }); const longLength = input.int('Long length', 21, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const exponential = input.bool('Use exponential MA', false); const chart = bars(); const shortEma = ta.ema(shortLength); const longEma = ta.ema(longLength); const shortSma = ta.sma(shortLength); const longSma = ta.sma(longLength); const line = plot.line('Price Oscillator', { color: color.study(5), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const price = chart.source(source); // Every average advances on every bar, whichever is drawn. const se = shortEma.update(price); const le = longEma.update(price); const ss = shortSma.update(price); const ls = longSma.update(price); const short = exponential ? se : ss; const long = exponential ? le : ls; line.set(((short - long) / long) * 100); }); ``` ### builtins/pvt.ts ```ts // Price Volume Trend, as TradingView's built-in PVT (`ta.pvt`): the running total of volume times // the close's percent change, from the second loaded bar. Its level depends on how much history is // loaded; its slope does not. Needs volume. indicator('Price Volume Trend', { shortName: 'PVT', precision: 0 }); const chart = bars(); const total = state.bar(na); const line = plot.line('PVT', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { const previous = chart.ago(1).close; const step = ((chart.close - previous) / previous) * chart.volume; if (!isNa(step)) total.value = nz(total.value) + step; line.set(total.value); }); ``` ### builtins/qqe.ts ```ts // QQE, Quantitative Qualitative Estimation (Roman Ignatov): a 5-bar EMA of the 14-bar RSI, and a // trailing line 4.236 times the smoothed size of its bar-to-bar moves away from it (Wilder's // smoothing over 2 × 14 − 1 bars, then an EMA), which ratchets towards it and flips side when the // RSI crosses it. A level at 50. Written from the description; no code copied. indicator('QQE', { shortName: 'QQE' }); const rsiLength = input.int('RSI length', 14, { min: 1, max: 500 }); const smoothing = input.int('RSI smoothing', 5, { min: 1, max: 500 }); const factor = input.float('QQE factor', 4.236, { min: 0.1, step: 0.1 }); const source = input.source('Source', 'close'); const wilders = rsiLength * 2 - 1; const chart = bars(); const rsi = ta.rsi(rsiLength); const rsiMa = ta.ema(smoothing); const moveAverage = ta.ema(wilders); const moveSmooth = ta.ema(wilders); const previousRsi = state.bar(na); const longBand = state.bar(na); const shortBand = state.bar(na); const trend = state.bar(1); const rsiLine = plot.line('RSI MA', { color: color.study(12), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const trailLine = plot.line('Fast trailing line', { color: color.study(9) }); plot.level('Middle', 50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.4) }); chart.onBar(() => { const r = rsiMa.update(rsi.update(chart.source(source))); const last = previousRsi.value; const delta = moveSmooth.update(moveAverage.update(Math.abs(r - last))) * factor; previousRsi.value = r; if (isNa(r) || isNa(delta)) { rsiLine.set(r); return; } const lastLong = longBand.value; const lastShort = shortBand.value; const newLong = r - delta; const newShort = r + delta; longBand.value = last > lastLong && r > lastLong ? Math.max(lastLong, newLong) : newLong; shortBand.value = last < lastShort && r < lastShort ? Math.min(lastShort, newShort) : newShort; // The RSI crossing the short band turns the trend up; crossing the long band, down. const crossesShort = !isNa(last) && !isNa(lastShort) && (last - lastShort) * (r - lastShort) < 0; const crossesLong = !isNa(last) && !isNa(lastLong) && (last - lastLong) * (r - lastLong) < 0; trend.value = crossesShort ? 1 : crossesLong ? -1 : trend.value; rsiLine.set(r); trailLine.set(trend.value === 1 ? longBand.value : shortBand.value); }); ``` ### builtins/range-filter.ts ```ts // Range Filter (Donovan Wall): the source filtered so it moves only when it travels more than a // smoothed range (an EMA over `period` bars of its bar-to-bar move, smoothed again over twice that // less one, times the multiplier), with bands that range either side. It starts at the price on // the first bar the range has a value. Written from the description; no code copied. indicator('Range Filter', { overlay: true, shortName: 'RF' }); const period = input.int('Sampling period', 100, { min: 1, max: 500 }); const multiplier = input.float('Range multiplier', 3, { min: 0.1, step: 0.1 }); const source = input.source('Source', 'close'); const chart = bars(); const move = ta.change(1); const average = ta.ema(period); const smooth = ta.ema(period * 2 - 1); const filter = state.bar(na); const line = plot.line('Filter', { color: color.up, width: 2 }); const upper = plot.line('High band', { color: color.withAlpha(color.study(12), 0.6), axisLabel: false }); const lower = plot.line('Low band', { color: color.withAlpha(color.study(12), 0.6), axisLabel: false }); const band = plot.fill('Band', upper, lower, { color: color.withAlpha(color.study(12), 0.06) }); chart.onBar(() => { const x = chart.source(source); const r = smooth.update(average.update(Math.abs(move.update(x)))) * multiplier; if (isNa(r)) return; const last = filter.value; const f = isNa(last) ? x : x > last ? (x - r < last ? last : x - r) : x + r > last ? last : x + r; filter.value = f; line.set(f, isNa(last) || f >= last ? color.up : color.down); upper.set(f + r); lower.set(f - r); }); ``` ### builtins/rci.ts ```ts // Rank Correlation Index: Spearman's rank correlation between the source and time over the last // `length` bars, times 100. 100 when every bar closed above the one before, −100 when every bar // closed below. Equal values share the mean of their ranks. Reads any bar type. indicator('Rank Correlation Index', { shortName: 'RCI' }); const length = input.int('Length', 10, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const history = series(length); const line = plot.line('RCI', { color: color.study(12), range: { min: -100, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', -80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { history.set(chart.source(source)); if (chart.index < length - 1) return; // Bar k counts back from this one: time rank (oldest 1) is length − k; the price rank is // 1 + the values below it + half the other values equal to it. let squares = 0; for (let k = 0; k < length; k += 1) { const v = history.ago(k); if (isNa(v)) return; let below = 0; let equal = 0; for (let j = 0; j < length; j += 1) { if (j === k) continue; const w = history.ago(j); if (w < v) below += 1; else if (w === v) equal += 1; } const d = 1 + below + equal / 2 - (length - k); squares += d * d; } line.set(100 * (1 - (6 * squares) / (length * (length * length - 1)))); }); ``` ### builtins/reflex.ts ```ts // Reflex (John Ehlers, TASC February 2020): how far a Super Smoother of the close (over half the // length) sits from the straight line through its value `length` bars ago and now, averaged over // those bars and divided by its running root mean square. It turns with the cycle and ignores // the trend. Reads any bar type. indicator('Ehlers Reflex', { shortName: 'Reflex' }); const length = input.int('Length', 20, { min: 2, max: 500 }); const period = length / 2; const a1 = Math.exp((-1.414 * Math.PI) / period); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / period); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const price = series(1); const filt = series(length); const meanSquare = state.bar(0); const line = plot.line('Reflex', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const x = chart.close; price.set(x); filt.set(chart.index < 2 ? x : (c1 * (x + price.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); if (chart.index < length) return; const slope = (filt.ago(length) - filt.value) / length; let sum = 0; for (let n = 1; n <= length; n += 1) sum += filt.value + n * slope - filt.ago(n); sum /= length; meanSquare.value = 0.04 * sum * sum + 0.96 * meanSquare.value; if (meanSquare.value > 0) line.set(sum / Math.sqrt(meanSquare.value)); }); ``` ### builtins/relative-volatility-index.ts ```ts // Relative Volatility Index (Donald Dorsey, TASC 1993), as TradingView's built-in RVI: an RSI built // from the source's standard deviation instead of its change: an EMA of the deviation on bars that // rose, over the same on every bar. Bands at 80 and 20. Reads any bar type. indicator('Relative Volatility Index', { shortName: 'RVI' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const emaLength = input.int('EMA length', 14, { min: 1, max: 500 }); const chart = bars(); const stdev = ta.stdev(length); const change = ta.change(1); const up = ta.ema(emaLength); const down = ta.ema(emaLength); const line = plot.line('RVI', { color: color.study(13), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(13), 0.08) }); chart.onBar(() => { const price = chart.source(source); const deviation = stdev.update(price); const c = change.update(price); const u = up.update(c <= 0 ? 0 : deviation); const d = down.update(c > 0 ? 0 : deviation); line.set((u / (u + d)) * 100); }); ``` ### builtins/relative-volume.ts ```ts // Relative Volume at Time: the bar's volume over the average volume of the bars at the same time // of day (UTC) on the previous `length` days that have one, so 2 means twice the usual volume for // this hour. It needs time bars of a minute to a day, and at least one earlier day; a day's bar // has only the days before it. A level at 1. Needs volume. indicator('Relative Volume at Time', { shortName: 'RVol' }); const length = input.int('Days', 10, { min: 1, max: 60 }); const chart = bars(); const usable = chart.interval.isTime && chart.interval.ms >= 60_000 && chart.interval.ms <= 86_400_000; // Per time of day, the volumes of the previous days, newest last, as text ("v1,v2,..."). const slots = state.map(usable ? Math.ceil(86_400_000 / chart.interval.ms) : 1); const columns = plot.histogram('Relative volume', { color: color.study(12), axisLabel: true }); plot.level('Usual', 1, { style: 'dotted', color: color.withAlpha(color.neutral, 0.45) }); chart.onBar(() => { if (!usable || isNa(chart.volume)) return; const key = String(chart.time % 86_400_000); const earlier = (slots.get(key) ?? '').split(',').filter((text) => text !== '').map(Number); if (earlier.length > 0) { const average = earlier.reduce((sum, v) => sum + v, 0) / earlier.length; columns.set(chart.volume / average, chart.volume >= average ? color.withAlpha(color.up, 0.6) : color.withAlpha(color.down, 0.6)); } }); // The bar's volume joins its time of day's list once the bar closes. chart.onClose(() => { if (!usable || isNa(chart.volume)) return; const key = String(chart.time % 86_400_000); const earlier = (slots.get(key) ?? '').split(',').filter((text) => text !== ''); earlier.push(String(chart.volume)); slots.set(key, earlier.slice(-length).join(',')); }); ``` ### builtins/roc.ts ```ts // Rate of change, as TradingView's built-in ROC: the percent change of the source over `length` // bars. A zero line. Reads any bar type. indicator('Rate Of Change', { shortName: 'ROC' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const roc = ta.roc(length); const line = plot.line('ROC', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(roc.update(chart.source(source))); }); ``` ### builtins/roofing-filter.ts ```ts // Roofing Filter (John Ehlers, Cycle Analytics for Traders, 2013): a two-pole high-pass filter that // removes cycles longer than `high-pass period` bars, then a Super Smoother that removes those // shorter than `low-pass period`, leaving the cycles in between around zero. Its first two values // are zero, as Ehlers' code starts. Reads any bar type. indicator('Ehlers Roofing Filter', { shortName: 'Roofing' }); const highPass = input.int('High-pass period', 48, { min: 2, max: 500 }); const lowPass = input.int('Low-pass period', 10, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const w = (0.707 * 2 * Math.PI) / highPass; const alpha = (Math.cos(w) + Math.sin(w) - 1) / Math.cos(w); const a1 = Math.exp((-1.414 * Math.PI) / lowPass); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / lowPass); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const price = series(2); const hp = series(2); const filt = series(2); const line = plot.line('Roofing', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const x = chart.source(source); price.set(x); if (chart.index < 2) { hp.set(0); filt.set(0); line.set(0); return; } hp.set((1 - alpha / 2) ** 2 * (x - 2 * price.ago(1) + price.ago(2)) + 2 * (1 - alpha) * hp.ago(1) - (1 - alpha) ** 2 * hp.ago(2)); filt.set((c1 * (hp.value + hp.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); line.set(filt.value); }); ``` ### builtins/rsi.ts ```ts // The relative strength index, as TradingView's built-in RSI: Wilder's smoothing (an RMA) of the // gains and the losses, each seeded with the simple average of the first `length` changes, so the // first value is on bar `length`. A bounded pane from 0 to 100 with quiet bands at 70 and 30 and // the middle at 50. Reads any bar type: it needs only the source, the close by default. indicator('RSI', { shortName: 'RSI' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const rsi = ta.rsi(length); const line = plot.line('RSI', { color: color.study(13), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', 70, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', 30, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(13), 0.08) }); chart.onBar(() => { line.set(rsi.update(chart.source(source))); }); ``` ### builtins/rvi.ts ```ts // Relative Vigor Index (John Ehlers, TASC 2002), as TradingView's built-in RVGI: the sum over // `length` bars of the symmetrically weighted close less open (weights 1, 2, 2, 1 over four bars) // over the same of high less low; the signal is the same 1, 2, 2, 1 average of that. Reads any bar // type. indicator('Relative Vigor Index', { shortName: 'RVGI' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const chart = bars(); const body = series(3); const span = series(3); const vigor = series(3); const numerator = ta.sma(length); const denominator = ta.sma(length); const rviLine = plot.line('RVGI', { color: color.study(4), margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(6) }); // TradingView's ta.swma: x[3] / 6 + x[2] / 3 + x[1] / 3 + x / 6. function swma(s: Series) { return s.ago(3) / 6 + s.ago(2) / 3 + s.ago(1) / 3 + s.value / 6; } chart.onBar(() => { body.set(chart.close - chart.open); span.set(chart.high - chart.low); // Sums as averages: the length cancels. const rvi = numerator.update(swma(body)) / denominator.update(swma(span)); vigor.set(rvi); rviLine.set(rvi); signalLine.set(swma(vigor)); }); ``` ### builtins/sma.ts ```ts // Simple moving average, as TradingView's built-in SMA: the mean of the last `length` values of // the source, the first on bar `length - 1`. The offset shifts the line right (positive) or left // (negative) by whole bars. Reads any bar type. indicator('SMA', { overlay: true, shortName: 'SMA' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const offset = input.int('Offset', 0, { min: -500, max: 500 }); const chart = bars(); const sma = ta.sma(length); const line = plot.line('SMA', { color: color.study(12), offset }); chart.onBar(() => { line.set(sma.update(chart.source(source))); }); ``` ### builtins/smi-ergodic-oscillator.ts ```ts // SMI Ergodic Oscillator (William Blau), as TradingView's built-in: the SMI Ergodic Indicator less // its signal line, as columns. Reads any bar type. indicator('SMI Ergodic Oscillator', { shortName: 'SMIO' }); const shortLength = input.int('Short length', 5, { min: 1, max: 500 }); const longLength = input.int('Long length', 20, { min: 1, max: 500 }); const signalLength = input.int('Signal line length', 5, { min: 1, max: 500 }); const chart = bars(); const change = ta.change(1); const moveLong = ta.ema(longLength); const moveShort = ta.ema(shortLength); const sizeLong = ta.ema(longLength); const sizeShort = ta.ema(shortLength); const signal = ta.ema(signalLength); const columns = plot.histogram('SMI Ergodic Oscillator', { color: color.up, axisLabel: true }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const c = change.update(chart.close); const ergodic = moveShort.update(moveLong.update(c)) / sizeShort.update(sizeLong.update(Math.abs(c))); const oscillator = ergodic - signal.update(ergodic); columns.set(oscillator, oscillator >= 0 ? color.withAlpha(color.up, 0.6) : color.withAlpha(color.down, 0.6)); }); ``` ### builtins/smi-ergodic.ts ```ts // SMI Ergodic Indicator (William Blau), as TradingView's built-in: the true strength index of the // close (its change double-smoothed by EMAs, long then short, over the same of its size), as a // ratio from −1 to 1, and its EMA as the signal. Reads any bar type. indicator('SMI Ergodic Indicator', { shortName: 'SMII' }); const shortLength = input.int('Short length', 5, { min: 1, max: 500 }); const longLength = input.int('Long length', 20, { min: 1, max: 500 }); const signalLength = input.int('Signal line length', 5, { min: 1, max: 500 }); const chart = bars(); const change = ta.change(1); const moveLong = ta.ema(longLength); const moveShort = ta.ema(shortLength); const sizeLong = ta.ema(longLength); const sizeShort = ta.ema(shortLength); const signal = ta.ema(signalLength); const indicatorLine = plot.line('SMI', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(8) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const c = change.update(chart.close); const ergodic = moveShort.update(moveLong.update(c)) / sizeShort.update(sizeLong.update(Math.abs(c))); indicatorLine.set(ergodic); signalLine.set(signal.update(ergodic)); }); ``` ### builtins/smi.ts ```ts // Stochastic Momentum Index (William Blau, TASC 1993), as TradingView's built-in SMI: the close's // distance from the middle of its `%K length`-bar range, over half that range, each double-smoothed // by EMAs over `%D length`, times 100; the signal an EMA of that. Bands at 40 and −40. Reads any // bar type: it needs the high, low and close. indicator('Stochastic Momentum Index', { shortName: 'SMI' }); const kLength = input.int('%K length', 10, { min: 1, max: 500 }); const dLength = input.int('%D length', 3, { min: 1, max: 500 }); const emaLength = input.int('EMA length', 3, { min: 1, max: 500 }); const chart = bars(); const highest = ta.highest(kLength); const lowest = ta.lowest(kLength); const distance1 = ta.ema(dLength); const distance2 = ta.ema(dLength); const range1 = ta.ema(dLength); const range2 = ta.ema(dLength); const signal = ta.ema(emaLength); const smiLine = plot.line('SMI', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('SMI-based EMA', { color: color.study(8) }); const upper = plot.level('Upper band', 40, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', -40, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { const top = highest.update(chart.high); const bottom = lowest.update(chart.low); const smi = (200 * distance2.update(distance1.update(chart.close - (top + bottom) / 2))) / range2.update(range1.update(top - bottom)); smiLine.set(smi); signalLine.set(signal.update(smi)); }); ``` ### builtins/smma.ts ```ts // Smoothed moving average, as TradingView's built-in SMMA: seeded with the simple average of the // first `length` values, then (previous × (length − 1) + value) / length. The same line as // Wilder's RMA (`ta.rma`). Reads any bar type. indicator('SMMA', { overlay: true, shortName: 'SMMA' }); const length = input.int('Length', 7, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const smma = ta.rma(length); const line = plot.line('SMMA', { color: color.study(5) }); chart.onBar(() => { line.set(smma.update(chart.source(source))); }); ``` ### builtins/smoothed-heikin-ashi.ts ```ts // Smoothed Heikin Ashi: the bar's open, high, low and close each smoothed by an EMA over `length` // bars, made into Heikin Ashi candles (the close the mean of the four, the open the midpoint of the // previous candle's open and close), and those smoothed again over `smoothing` bars. Drawn as // candles over the chart's, up-coloured when they close above their open. Reads any bar type. indicator('Smoothed Heikin Ashi', { overlay: true, shortName: 'SHA' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const smoothing = input.int('Smoothing', 10, { min: 1, max: 500 }); const chart = bars(); const o = ta.ema(length); const h = ta.ema(length); const l = ta.ema(length); const c = ta.ema(length); const haOpen = state.bar(na); const haClose = state.bar(na); const so = ta.ema(smoothing); const sh = ta.ema(smoothing); const sl = ta.ema(smoothing); const sc = ta.ema(smoothing); const candles = plot.candles('Smoothed Heikin Ashi', { color: color.up }); chart.onBar(() => { const open = o.update(chart.open); const high = h.update(chart.high); const low = l.update(chart.low); const close = c.update(chart.close); // The open is the midpoint of the previous candle's open and close; the first is the bar's own. if (!isNa(open)) { const last = haOpen.value; haOpen.value = isNa(last) ? (open + close) / 2 : (last + haClose.value) / 2; haClose.value = (open + high + low + close) / 4; } const ready = !isNa(open); // Every smoothing updates on every bar, with na until the candles start, which an EMA skips. const bar = { open: so.update(ready ? haOpen.value : na), high: sh.update(ready ? Math.max(high, haOpen.value, haClose.value) : na), low: sl.update(ready ? Math.min(low, haOpen.value, haClose.value) : na), close: sc.update(ready ? haClose.value : na), }; if (isNa(bar.open)) return; candles.set(bar, bar.close >= bar.open ? color.up : color.down); }); ``` ### builtins/squeeze-momentum.ts ```ts // Squeeze Momentum, after John Carter's TTM Squeeze as LazyBear popularised it: the momentum, the // end of a 20-bar regression line through the close's distance from the middle of its range and // its average, as columns (strong while growing, light while shrinking); and on the zero line a // dot for the squeeze, down-coloured while the Bollinger Bands sit inside the Keltner Channels // (volatility coiled), up-coloured once they leave. Written from the description; no code copied. indicator('Squeeze Momentum', { shortName: 'SQZMOM' }); const bbLength = input.int('BB length', 20, { min: 1, max: 500 }); const bbMultiplier = input.float('BB multiplier', 2, { min: 0.1, step: 0.1 }); const kcLength = input.int('KC length', 20, { min: 1, max: 500 }); const kcMultiplier = input.float('KC multiplier', 1.5, { min: 0.1, step: 0.1 }); const chart = bars(); const bands = ta.bb(bbLength, bbMultiplier); const kcBasis = ta.sma(kcLength); const kcRange = ta.sma(kcLength); const trueRange = ta.tr(true); const highest = ta.highest(kcLength); const lowest = ta.lowest(kcLength); const closeAverage = ta.sma(kcLength); const regression = ta.linreg(kcLength, 0); const previous = state.bar(na); const columns = plot.histogram('Momentum', { color: color.up, axisLabel: true }); const squeeze = plot.points('Squeeze', { shape: 'cross', color: color.neutral, axisLabel: false }); chart.onBar(() => { const b = bands.update(chart.close); const basis = kcBasis.update(chart.close); const range = kcRange.update(trueRange.update(chart)); const upperKc = basis + range * kcMultiplier; const lowerKc = basis - range * kcMultiplier; const middle = ((highest.update(chart.high) + lowest.update(chart.low)) / 2 + closeAverage.update(chart.close)) / 2; const value = regression.update(chart.close - middle); const grew = previous.value < value; columns.set(value, value >= 0 ? (grew ? color.up : color.withAlpha(color.up, 0.45)) : (grew ? color.withAlpha(color.down, 0.45) : color.down)); previous.value = value; if (isNa(b.upper) || isNa(upperKc)) return; const on = b.lower > lowerKc && b.upper < upperKc; const off = b.lower < lowerKc && b.upper > upperKc; squeeze.set(0, on ? color.down : off ? color.up : color.neutral); }); ``` ### builtins/ssl-channel.ts ```ts // SSL Channel (Erwin Beckers): simple averages of the highs and of the lows over `length` bars; the // close above the high average turns it up and below the low average turns it down, and the two // lines swap so the up line is on top in an uptrend. Written from the description; no code copied. indicator('SSL Channel', { overlay: true, shortName: 'SSL' }); const length = input.int('Length', 10, { min: 1, max: 500 }); const chart = bars(); const highs = ta.sma(length); const lows = ta.sma(length); const side = state.bar(0); const down = plot.line('SSL down', { color: color.down }); const up = plot.line('SSL up', { color: color.up }); chart.onBar(() => { const h = highs.update(chart.high); const l = lows.update(chart.low); if (isNa(h) || isNa(l)) return; side.value = chart.close > h ? 1 : chart.close < l ? -1 : side.value; down.set(side.value < 0 ? h : l); up.set(side.value < 0 ? l : h); }); ``` ### builtins/stdev.ts ```ts // Standard Deviation, as TradingView's built-in: the population standard deviation of the source // over `length` bars (divided by length, not length − 1), in price units. Reads any bar type. indicator('Standard Deviation', { shortName: 'StDev' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); // In price units but small beside the price: the market's decimals and two more. function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const stdev = ta.stdev(length); const line = plot.line('StDev', { color: color.study(12), precision: decimalsOf(chart.market.tickSize) }); chart.onBar(() => { line.set(stdev.update(chart.source(source))); }); ``` ### builtins/stoch-rsi.ts ```ts // Stochastic RSI (Tushar Chande and Stanley Kroll, 1994), as TradingView's built-in: the stochastic // of the RSI against its own range over `stochastic length` bars, %K averaged over `K` bars and %D // over `D`. Bands at 80 and 20. The RSI has no value for its first `RSI length` bars, so the range // then fills from its first value. Reads any bar type. indicator('Stoch RSI', { shortName: 'Stoch RSI' }); const smoothK = input.int('K', 3, { min: 1, max: 500 }); const smoothD = input.int('D', 3, { min: 1, max: 500 }); const rsiLength = input.int('RSI length', 14, { min: 1, max: 500 }); const stochLength = input.int('Stochastic length', 14, { min: 1, max: 500 }); const source = input.source('RSI source', 'close'); const chart = bars(); const rsi = ta.rsi(rsiLength); const stoch = ta.stoch(stochLength); const kAverage = ta.sma(smoothK); const dAverage = ta.sma(smoothD); const kLine = plot.line('K', { color: color.study(12), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const dLine = plot.line('D', { color: color.study(8) }); const upper = plot.level('Upper band', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { const r = rsi.update(chart.source(source)); const k = kAverage.update(stoch.update(r, r, r)); kLine.set(k); dLine.set(dAverage.update(k)); }); ``` ### builtins/stochastic.ts ```ts // The stochastic oscillator, as TradingView's built-in Stochastic: %K is where the close sits in // the range of the last `%K length` bars' highs and lows, from 0 to 100, averaged over `%K smoothing` // bars (3 by default; 1 leaves it as it is); %D is a simple average of %K. Bands at 80 and 20, the middle at 50. // Reads any bar type: it needs the high, low and close. indicator('Stochastic', { shortName: 'Stoch' }); const kLength = input.int('%K length', 14, { min: 1, max: 500 }); const kSmoothing = input.int('%K smoothing', 3, { min: 1, max: 500 }); const dSmoothing = input.int('%D smoothing', 3, { min: 1, max: 500 }); const chart = bars(); const raw = ta.stoch(kLength); const smoothK = ta.sma(kSmoothing); const smoothD = ta.sma(dSmoothing); const kLine = plot.line('%K', { color: color.study(12), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); const dLine = plot.line('%D', { color: color.study(8) }); const upper = plot.level('Upper band', 80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', 50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', 20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(12), 0.08) }); chart.onBar(() => { const k = smoothK.update(raw.update(chart.close, chart.high, chart.low)); kLine.set(k); dLine.set(smoothD.update(k)); }); ``` ### builtins/super-smoother.ts ```ts // Super Smoother (John Ehlers, Cycle Analytics for Traders, 2013): a two-pole Butterworth filter // of the source, averaging each value with the one before, that cuts cycles shorter than `period` // bars with little lag. Its first two values are the source itself. Reads any bar type. indicator('Ehlers Super Smoother', { overlay: true, shortName: 'SuperSmoother' }); const period = input.int('Period', 10, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const a1 = Math.exp((-1.414 * Math.PI) / period); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / period); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const price = series(1); const filt = series(2); const line = plot.line('Super Smoother', { color: color.study(11) }); chart.onBar(() => { const x = chart.source(source); price.set(x); filt.set(chart.index < 2 ? x : (c1 * (x + price.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); line.set(filt.value); }); ``` ### builtins/supertrend.ts ```ts // Supertrend (Olivier Seban), as TradingView's built-in: bands the factor times the ATR (Wilder's, // over 10 bars) either side of hl2, each ratcheting only towards price until the close crosses // it. The line below price while the trend is up, above it while it is down, broken where the // trend turns. Reads any bar type: it needs the high, low and close. indicator('Supertrend', { overlay: true, shortName: 'Supertrend' }); const atrLength = input.int('ATR length', 10, { min: 1, max: 500 }); const factor = input.float('Factor', 3, { min: 0.01, step: 0.01 }); const chart = bars(); const trend = ta.supertrend(factor, atrLength); const up = plot.line('Up trend', { color: color.up }); const down = plot.line('Down trend', { color: color.down }); chart.onBar(() => { const value = trend.update(chart); if (value.direction > 0) up.set(value.line); else if (value.direction < 0) down.set(value.line); }); ``` ### builtins/t3.ts ```ts // Tillson T3 (Tim Tillson, TASC 1998): six EMAs in a chain, combined with weights from the volume // factor `v`, which trades smoothness against lag (0 is a triple EMA chain, 1 is DEMA applied // three times). Each EMA is seeded like `ta.ema`, so the first value is on bar 6 × (length − 1); // TA-Lib's T3 seeds its chain differently and agrees once the seeds have decayed. indicator('T3', { overlay: true, shortName: 'T3' }); const length = input.int('Length', 5, { min: 1, max: 500 }); const factor = input.float('Volume factor', 0.7, { min: 0, max: 1, step: 0.05 }); const source = input.source('Source', 'close'); const chart = bars(); const e1 = ta.ema(length); const e2 = ta.ema(length); const e3 = ta.ema(length); const e4 = ta.ema(length); const e5 = ta.ema(length); const e6 = ta.ema(length); const a2 = factor * factor; const a3 = a2 * factor; const c1 = -a3; const c2 = 3 * a2 + 3 * a3; const c3 = -6 * a2 - 3 * factor - 3 * a3; const c4 = 1 + 3 * factor + a3 + 3 * a2; const line = plot.line('T3', { color: color.study(3) }); chart.onBar(() => { const x3 = e3.update(e2.update(e1.update(chart.source(source)))); const x4 = e4.update(x3); const x5 = e5.update(x4); const x6 = e6.update(x5); line.set(c1 * x6 + c2 * x5 + c3 * x4 + c4 * x3); }); ``` ### builtins/tema.ts ```ts // Triple exponential moving average (Patrick Mulloy, TASC 1994), as TradingView's built-in TEMA: // 3 × EMA − 3 × EMA(EMA) + EMA(EMA(EMA)), each over `length` and seeded like `ta.ema`, so the // first value is on bar 3 × (length − 1). Reads any bar type. indicator('TEMA', { overlay: true, shortName: 'TEMA' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const e1 = ta.ema(length); const e2 = ta.ema(length); const e3 = ta.ema(length); const line = plot.line('TEMA', { color: color.study(7) }); chart.onBar(() => { const a = e1.update(chart.source(source)); const b = e2.update(a); const c = e3.update(b); line.set(3 * a - 3 * b + c); }); ``` ### builtins/trendflex.ts ```ts // Trendflex (John Ehlers, TASC February 2020): how far a Super Smoother of the close (over half the // length) sits above its own values over the last `length` bars, averaged and divided by its // running root mean square. Reflex's partner: it keeps the trend. Reads any bar type. indicator('Ehlers Trendflex', { shortName: 'Trendflex' }); const length = input.int('Length', 20, { min: 2, max: 500 }); const period = length / 2; const a1 = Math.exp((-1.414 * Math.PI) / period); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / period); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const price = series(1); const filt = series(length); const meanSquare = state.bar(0); const line = plot.line('Trendflex', { color: color.study(8), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const x = chart.close; price.set(x); filt.set(chart.index < 2 ? x : (c1 * (x + price.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); if (chart.index < length) return; let sum = 0; for (let n = 1; n <= length; n += 1) sum += filt.value - filt.ago(n); sum /= length; meanSquare.value = 0.04 * sum * sum + 0.96 * meanSquare.value; if (meanSquare.value > 0) line.set(sum / Math.sqrt(meanSquare.value)); }); ``` ### builtins/trix.ts ```ts // TRIX (Jack Hutson, 1980s), as TradingView's built-in: the one-bar change of a triple EMA of the // log of the close, times 10,000. A zero line. Reads any bar type. indicator('TRIX', { shortName: 'TRIX' }); const length = input.int('Length', 18, { min: 1, max: 500 }); const chart = bars(); const e1 = ta.ema(length); const e2 = ta.ema(length); const e3 = ta.ema(length); const change = ta.change(1); const line = plot.line('TRIX', { color: color.study(6), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { line.set(10_000 * change.update(e3.update(e2.update(e1.update(Math.log(chart.close)))))); }); ``` ### builtins/tsi.ts ```ts // True strength index (William Blau, 1991), as TradingView's built-in TSI: the bar-to-bar change of // the close, double-smoothed by EMAs (long, then short), over the same of its absolute value, times // 100; the signal an EMA of that. Reads any bar type. indicator('True Strength Index', { shortName: 'TSI' }); const longLength = input.int('Long length', 25, { min: 1, max: 500 }); const shortLength = input.int('Short length', 13, { min: 1, max: 500 }); const signalLength = input.int('Signal length', 13, { min: 1, max: 500 }); const chart = bars(); const change = ta.change(1); const moveLong = ta.ema(longLength); const moveShort = ta.ema(shortLength); const sizeLong = ta.ema(longLength); const sizeShort = ta.ema(shortLength); const signal = ta.ema(signalLength); const tsiLine = plot.line('True Strength Index', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(6) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const c = change.update(chart.close); const tsi = (100 * moveShort.update(moveLong.update(c))) / sizeShort.update(sizeLong.update(Math.abs(c))); tsiLine.set(tsi); signalLine.set(signal.update(tsi)); }); ``` ### builtins/ulcer-index.ts ```ts // Ulcer Index (Peter Martin, 1987): the root mean square of the percent drawdown of the source // from its highest value over `length` bars, over the same `length` bars. It measures the depth // and the length of falls, not the swings up. Reads any bar type. indicator('Ulcer Index', { shortName: 'UI' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const highest = ta.highest(length); const squares = ta.sma(length); const line = plot.line('Ulcer Index', { color: color.study(6), margins: { top: 0.1, bottom: 0.1 } }); chart.onBar(() => { const price = chart.source(source); const top = highest.update(price); const drawdown = (100 * (price - top)) / top; line.set(Math.sqrt(squares.update(drawdown * drawdown))); }); ``` ### builtins/ultimate-oscillator.ts ```ts // Ultimate Oscillator (Larry Williams, 1976), as TradingView's built-in UO: buying pressure (the // close less the lower of the low and the previous close) over the true range, summed over 7, 14 // and 28 bars and weighted 4, 2 and 1. From 0 to 100, bands at 70 and 30. Reads any bar type: it // needs the high, low and close. indicator('Ultimate Oscillator', { shortName: 'UO' }); const fastLength = input.int('Fast length', 7, { min: 1, max: 500 }); const middleLength = input.int('Middle length', 14, { min: 1, max: 500 }); const slowLength = input.int('Slow length', 28, { min: 1, max: 500 }); const chart = bars(); const pressureFast = ta.sma(fastLength); const rangeFast = ta.sma(fastLength); const pressureMiddle = ta.sma(middleLength); const rangeMiddle = ta.sma(middleLength); const pressureSlow = ta.sma(slowLength); const rangeSlow = ta.sma(slowLength); const line = plot.line('UO', { color: color.study(11), range: { min: 0, max: 100 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Upper band', 70, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Lower band', 30, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); chart.onBar(() => { const previousClose = chart.ago(1).close; // Na on the first bar, which has no previous close. const floor = isNa(previousClose) ? na : Math.min(chart.low, previousClose); const ceiling = isNa(previousClose) ? na : Math.max(chart.high, previousClose); const pressure = chart.close - floor; const range = ceiling - floor; // Sums over each window as averages: the length cancels in each ratio. const fast = pressureFast.update(pressure) / rangeFast.update(range); const middle = pressureMiddle.update(pressure) / rangeMiddle.update(range); const slow = pressureSlow.update(pressure) / rangeSlow.update(range); line.set((100 * (4 * fast + 2 * middle + slow)) / 7); }); ``` ### builtins/universal-oscillator.ts ```ts // Universal Oscillator (John Ehlers, TASC January 2015): the two-bar change of the close, halved, // smoothed by a Super Smoother over the band edge, then divided by its decaying peak (falling 0.9% // a bar), so it swings between −1 and 1 whatever the market's volatility. Reads any bar type. indicator('Universal Oscillator', { shortName: 'UniOsc' }); const bandEdge = input.int('Band edge', 20, { min: 2, max: 500 }); const a1 = Math.exp((-1.414 * Math.PI) / bandEdge); const c2 = 2 * a1 * Math.cos((1.414 * Math.PI) / bandEdge); const c3 = -a1 * a1; const c1 = 1 - c2 - c3; const chart = bars(); const price = series(2); const noise = series(1); const filt = series(2); const peak = state.bar(0); const line = plot.line('Universal', { color: color.study(12), range: { min: -1, max: 1 }, margins: { top: 0.08, bottom: 0.08 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const x = chart.close; price.set(x); noise.set(chart.index < 2 ? 0 : (x - price.ago(2)) / 2); filt.set(chart.index < 2 ? 0 : (c1 * (noise.value + noise.ago(1))) / 2 + c2 * filt.ago(1) + c3 * filt.ago(2)); peak.value = 0.991 * peak.value; if (Math.abs(filt.value) > peak.value) peak.value = Math.abs(filt.value); if (peak.value > 0) line.set(filt.value / peak.value); }); ``` ### builtins/ut-bot.ts ```ts // UT Bot alerts, as the strategy circulates: an ATR trailing stop (the key times the ATR over `ATR // period` bars behind the close, ratcheting while the close stays on its side, flipping when it // crosses), with a buy label where the close crosses above it and a sell label where it crosses // below. Written from the description; no code copied. indicator('UT Bot', { overlay: true, shortName: 'UT Bot' }); const key = input.float('Key value', 1, { min: 0.1, step: 0.1 }); const atrPeriod = input.int('ATR period', 10, { min: 1, max: 500 }); const chart = bars(); const atr = ta.atr(atrPeriod); const trail = state.bar(na); const line = plot.line('Trailing stop', { color: color.study(9) }); const buys = plot.shape('Buy', { shape: 'labelUp', location: 'absolute', text: 'Buy', color: color.up }); const sells = plot.shape('Sell', { shape: 'labelDown', location: 'absolute', text: 'Sell', color: color.down }); chart.onBar(() => { const loss = key * atr.update(chart); const price = chart.close; const last = chart.ago(1).close; const before = trail.value; if (isNa(loss)) return; const t = isNa(before) ? price - loss : price > before && last > before ? Math.max(before, price - loss) : price < before && last < before ? Math.min(before, price + loss) : price > before ? price - loss : price + loss; trail.value = t; line.set(t); buys.set(!isNa(before) && last <= before && price > t, { price: chart.low }); sells.set(!isNa(before) && last >= before && price < t, { price: chart.high }); }); ``` ### builtins/vidya.ts ```ts // Variable Index Dynamic Average (Tushar Chande, 1992): an EMA over `length` bars whose alpha is // scaled by the size of the Chande Momentum Oscillator over `momentum length` bars, so it moves // with strong momentum and holds still without it. It starts at the price on the first bar the // oscillator has a value. Reads any bar type. indicator('VIDYA', { overlay: true, shortName: 'VIDYA' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const momentumLength = input.int('Momentum length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const alpha = 2 / (length + 1); const chart = bars(); const change = ta.change(1); const gains = ta.sma(momentumLength); const losses = ta.sma(momentumLength); const vidya = state.bar(na); const line = plot.line('VIDYA', { color: color.study(4) }); chart.onBar(() => { const x = chart.source(source); const m = change.update(x); const up = gains.update(isNa(m) ? na : Math.max(m, 0)); const down = losses.update(isNa(m) ? na : Math.max(-m, 0)); const k = up + down === 0 ? 0 : Math.abs((up - down) / (up + down)); if (isNa(k)) return; vidya.value = isNa(vidya.value) ? x : alpha * k * x + (1 - alpha * k) * vidya.value; line.set(vidya.value); }); ``` ### builtins/volatility-stop.ts ```ts // Volatility Stop, as TradingView's built-in: a stop the multiplier times the ATR (Wilder's, over // `length` bars) behind the extreme of the source since the trend turned, ratcheting only towards // price. When the source crosses it the trend turns and the stop starts again from the source. // Drawn as crosses, in the trend's colour. Reads any bar type. indicator('Volatility Stop', { overlay: true, shortName: 'VStop' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const multiplier = input.float('Multiplier', 2, { min: 0.1, step: 0.1 }); const chart = bars(); const atr = ta.atr(length); const trueRange = ta.tr(false); const highest = state.bar(na); const lowest = state.bar(na); const uptrend = state.bar(true); const stop = state.bar(na); const dots = plot.points('Volatility Stop', { shape: 'cross', color: color.up }); chart.onBar(() => { const price = chart.source(source); const a = atr.update(chart) * multiplier; const tr = trueRange.update(chart); if (isNa(price)) return; const range = isNa(a) ? tr : a; highest.value = isNa(highest.value) ? price : Math.max(highest.value, price); lowest.value = isNa(lowest.value) ? price : Math.min(lowest.value, price); const was = uptrend.value; const next = was ? Math.max(stop.value, highest.value - range) : Math.min(stop.value, lowest.value + range); stop.value = isNa(next) ? price : next; uptrend.value = price - stop.value >= 0; if (uptrend.value !== was) { highest.value = price; lowest.value = price; stop.value = uptrend.value ? price - range : price + range; } dots.set(stop.value, uptrend.value ? color.up : color.down); }); ``` ### builtins/volume-index.ts ```ts // Positive and Negative Volume Index (Paul Dysart, 1930s; Norman Fosback, 1976): each starts at // 1,000 and moves with the close's percent change, the positive index only on bars whose volume // rose and the negative only on bars whose volume fell. The negative one follows the quiet days // that informed money is said to trade. Needs volume. indicator('Positive and Negative Volume Index', { shortName: 'PVI/NVI' }); const chart = bars(); const positive = state.bar(1000); const negative = state.bar(1000); const pviLine = plot.line('PVI', { color: color.up, margins: { top: 0.1, bottom: 0.1 } }); const nviLine = plot.line('NVI', { color: color.down }); chart.onBar(() => { const previous = chart.ago(1); const move = (chart.close - previous.close) / previous.close; if (!isNa(move) && chart.volume > previous.volume) positive.value += move * positive.value; if (!isNa(move) && chart.volume < previous.volume) negative.value += move * negative.value; pviLine.set(positive.value); nviLine.set(negative.value); }); ``` ### builtins/volume-oscillator.ts ```ts // Volume Oscillator, as TradingView's built-in: the 5-bar EMA of volume less its 10-bar EMA, as a // percentage of the 10-bar one. Above zero, volume is picking up. Needs volume. indicator('Volume Oscillator', { shortName: 'Volume Osc' }); const shortLength = input.int('Short length', 5, { min: 1, max: 500 }); const longLength = input.int('Long length', 10, { min: 1, max: 500 }); const chart = bars(); const short = ta.ema(shortLength); const long = ta.ema(longLength); const line = plot.line('Volume Osc', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const s = short.update(chart.volume); const l = long.update(chart.volume); line.set((100 * (s - l)) / l); }); ``` ### builtins/volume.ts ```ts // Volume at the foot of the price pane, each bar in its candle's direction (close at or above // open is up), on a hidden scale of its own. indicator('Volume', { overlay: true }); const upColor = input.color('Up colour', null); const downColor = input.color('Down colour', null); const opacity = input.float('Opacity', 0.3, { min: 0.1, max: 1, step: 0.05 }); const chart = bars(); const up = color.withAlpha(upColor ?? color.up, opacity); const down = color.withAlpha(downColor ?? color.down, opacity); // Bars that close on volume all hold the same amount, so their histogram would be a flat wall. const flat = chart.interval.code.startsWith('v'); const volume = plot.histogram('Volume', { band: { top: 0.84, bottom: 0 }, axisLabel: false }); chart.onBar(() => { if (flat || isNa(chart.volume)) return; volume.set(chart.volume, chart.close >= chart.open ? up : down); }); ``` ### builtins/vortex.ts ```ts // Vortex indicator (Etienne Botes and Douglas Siepman, TASC 2010), as TradingView's built-in: VI+ // is the sum over `period` bars of |high − previous low|, VI− of |low − previous high|, each // divided by the sum of the true range over the same bars. Reads any bar type: it needs the high, // low and close. indicator('Vortex Indicator', { shortName: 'VI' }); const period = input.int('Period', 14, { min: 2, max: 500 }); const chart = bars(); const plusMoves = ta.sma(period); const minusMoves = ta.sma(period); const ranges = ta.sma(period); const trueRange = ta.tr(true); const plus = plot.line('VI +', { color: color.study(12) }); const minus = plot.line('VI -', { color: color.study(6) }); chart.onBar(() => { const prev = chart.ago(1); // Sums over the window as averages: the length cancels in the ratio. const p = plusMoves.update(Math.abs(chart.high - prev.low)); const m = minusMoves.update(Math.abs(chart.low - prev.high)); const r = ranges.update(trueRange.update(chart)); plus.set(p / r); minus.set(m / r); }); ``` ### builtins/voss.ts ```ts // Voss Predictive Filter (John Ehlers, TASC August 2019): a two-pole band-pass filter of the close // centred on `period` bars, and the Voss filter, which leads it by about `predict` bars by // subtracting a weighted sum of its own past values. Where Voss crosses the band-pass, the cycle // turns. The filters' first five values are zero, as Ehlers' code starts. Reads any bar type. indicator('Voss Predictive Filter', { shortName: 'Voss' }); const period = input.int('Period', 20, { min: 2, max: 500 }); const predict = input.float('Predict', 3, { min: 0.5, max: 20, step: 0.5 }); const bandwidth = input.float('Bandwidth', 0.25, { min: 0.01, max: 1, step: 0.05 }); const order = Math.round(3 * predict); const f1 = Math.cos((2 * Math.PI) / period); const g1 = Math.cos((bandwidth * 2 * Math.PI) / period); const s1 = 1 / g1 - Math.sqrt(1 / (g1 * g1) - 1); const chart = bars(); const price = series(2); const filt = series(2); const voss = series(Math.max(order, 1)); const filtLine = plot.line('Filter', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); const vossLine = plot.line('Voss', { color: color.study(6) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const x = chart.close; price.set(x); if (chart.index <= 5) { filt.set(0); voss.set(0); return; } filt.set(0.5 * (1 - s1) * (x - price.ago(2)) + f1 * (1 + s1) * filt.ago(1) - s1 * filt.ago(2)); let sum = 0; for (let count = 0; count < order; count += 1) sum += ((count + 1) / order) * nz(voss.ago(order - count)); voss.set(((3 + order) / 2) * filt.value - sum); filtLine.set(filt.value); vossLine.set(voss.value); }); ``` ### builtins/vw-macd.ts ```ts // Volume-weighted MACD (Buff Dormeier): the 12-bar volume-weighted moving average of the close less // the 26-bar one, a 9-bar EMA of that as the signal, and the difference as columns. Needs volume. indicator('Volume-Weighted MACD', { shortName: 'VW-MACD' }); const fastLength = input.int('Fast length', 12, { min: 1, max: 500 }); const slowLength = input.int('Slow length', 26, { min: 1, max: 500 }); const signalLength = input.int('Signal smoothing', 9, { min: 1, max: 500 }); // In price units but small beside the price: the market's decimals and two more. function decimalsOf(step: number) { return step > 0 ? Math.max(0, Math.min(10, Math.ceil(-Math.log10(step) - 1e-9) + 2)) : undefined; } const chart = bars(); const fast = ta.vwma(fastLength); const slow = ta.vwma(slowLength); const signal = ta.ema(signalLength); const precision = decimalsOf(chart.market.tickSize); const histogram = plot.histogram('Histogram', { color: color.up, precision, axisLabel: false }); const macdLine = plot.line('MACD', { color: color.study(12), precision, margins: { top: 0.1, bottom: 0.1 } }); const signalLine = plot.line('Signal', { color: color.study(8), precision }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); chart.onBar(() => { const macd = fast.update(chart.close, chart.volume) - slow.update(chart.close, chart.volume); const s = signal.update(macd); const h = macd - s; histogram.set(h, h >= 0 ? color.withAlpha(color.up, 0.6) : color.withAlpha(color.down, 0.6)); macdLine.set(macd); signalLine.set(s); }); ``` ### builtins/vwap-bands.ts ```ts // VWAP with standard deviation bands: the volume-weighted average price from each bar's quote and // base volume, as the chart's VWAP, and bands 1, 2 and 3 volume-weighted standard deviations of // the bars' own average prices around it. Resets at 00:00 UTC, or each week or month. The spread // of prices inside a bar is not in the deviation, only the spread between bars. Needs volume. indicator('VWAP Bands', { overlay: true, shortName: 'VWAP Bands' }); const reset = input.choice('Reset', 'session', ['session', 'week', 'month']); const multiplier1 = input.float('Band 1', 1, { min: 0, step: 0.5 }); const multiplier2 = input.float('Band 2', 2, { min: 0, step: 0.5 }); const multiplier3 = input.float('Band 3', 3, { min: 0, step: 0.5 }); const chart = bars(); const unit = reset === 'session' ? 'day' : reset; const base = state.bar(0); const quote = state.bar(0); const squares = state.bar(0); const period = state.bar(na); const vwap = plot.line('VWAP', { color: color.study(12), width: 2 }); const upper1 = plot.line('Upper 1', { color: color.withAlpha(color.study(10), 0.8) }); const lower1 = plot.line('Lower 1', { color: color.withAlpha(color.study(10), 0.8) }); const upper2 = plot.line('Upper 2', { color: color.withAlpha(color.study(9), 0.8) }); const lower2 = plot.line('Lower 2', { color: color.withAlpha(color.study(9), 0.8) }); const upper3 = plot.line('Upper 3', { color: color.withAlpha(color.study(6), 0.8) }); const lower3 = plot.line('Lower 3', { color: color.withAlpha(color.study(6), 0.8) }); const band = plot.fill('Band 1 fill', upper1, lower1, { color: color.withAlpha(color.study(10), 0.06) }); chart.onBar(() => { const start = clock.startOf(chart.time, unit); if (start !== period.value) { period.value = start; base.value = 0; quote.value = 0; squares.value = 0; } const v = chart.volume; const q = chart.quoteVolume; if (isNa(v) || isNa(q) || !(v > 0)) return; const price = q / v; base.value += v; quote.value += q; squares.value += price * price * v; const mean = quote.value / base.value; const deviation = Math.sqrt(Math.max(0, squares.value / base.value - mean * mean)); vwap.set(mean); upper1.set(mean + multiplier1 * deviation); lower1.set(mean - multiplier1 * deviation); upper2.set(mean + multiplier2 * deviation); lower2.set(mean - multiplier2 * deviation); upper3.set(mean + multiplier3 * deviation); lower3.set(mean - multiplier3 * deviation); }); ``` ### builtins/vwap.ts ```ts // Volume-weighted average price from each bar's quote and base volume: exact, never estimated // from OHLC. It resets at 00:00 UTC by default (TradingView's session anchor), or at each week or // month. With an anchor time picked on the chart it runs from that bar on, as the chart's // anchored VWAP did. A bar without both volumes has no value. indicator('VWAP', { overlay: true }); const reset = input.choice('Reset', 'session', ['session', 'week', 'month']); const anchor = input.time('Anchor time', null, { tooltip: 'Pick a bar on the chart to anchor the VWAP there.' }); const chart = bars(); const anchored = anchor !== null; const vwap = ta.vwap(anchored ? anchor : reset); // The session line is drawn on intraday bars only: on a daily bar each session is one bar. const drawn = anchored || reset !== 'session' || !chart.interval.isTime || chart.interval.ms < 86_400_000; const line = plot.line(anchored ? 'Anchored VWAP' : 'VWAP', { width: anchored ? 2 : 1, style: 'dashed', axisLabel: anchored }); const unit = reset === 'session' ? 'day' : reset; chart.onBar(() => { const value = vwap.update(chart); if (!drawn) return; // A point's colour paints the segment that starts at it: the last bar of a period draws // nothing onward, so the line breaks at the reset instead of jumping. const periodEnds = !anchored && clock.startOf(chart.closeTime, unit) === chart.closeTime; line.set(value, periodEnds ? color.none : null); }); ``` ### builtins/vwma.ts ```ts // Volume-weighted moving average, as TradingView's built-in VWMA: the source weighted by each // bar's volume over the last `length` bars. A bar without volume leaves the window na until it // passes. On bars that close on volume every bar weighs the same, so it equals the SMA there. indicator('VWMA', { overlay: true, shortName: 'VWMA' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const offset = input.int('Offset', 0, { min: -500, max: 500 }); const chart = bars(); const vwma = ta.vwma(length); const line = plot.line('VWMA', { color: color.study(13), offset }); chart.onBar(() => { line.set(vwma.update(chart.source(source), chart.volume)); }); ``` ### builtins/wavetrend.ts ```ts // WaveTrend, as LazyBear popularised it: hlc3's distance from its 10-bar EMA, over 0.015 times the // EMA of that distance's size (a CCI-like channel index), smoothed by a 21-bar EMA (WT1), and a // 4-bar average of that (WT2). Levels at ±60 and ±53. Written from the description; no code copied. indicator('WaveTrend', { shortName: 'WT' }); const channelLength = input.int('Channel length', 10, { min: 1, max: 500 }); const averageLength = input.int('Average length', 21, { min: 1, max: 500 }); const chart = bars(); const esa = ta.ema(channelLength); const deviation = ta.ema(channelLength); const tci = ta.ema(averageLength); const signal = ta.sma(4); const wt1 = plot.line('WT1', { color: color.study(4), margins: { top: 0.1, bottom: 0.1 } }); const wt2 = plot.line('WT2', { color: color.study(6) }); plot.level('Overbought 1', 60, { style: 'dashed', color: color.withAlpha(color.neutral, 0.4) }); plot.level('Overbought 2', 53, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('Oversold 2', -53, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('Oversold 1', -60, { style: 'dashed', color: color.withAlpha(color.neutral, 0.4) }); chart.onBar(() => { const ap = chart.source('hlc3'); const e = esa.update(ap); const d = deviation.update(Math.abs(ap - e)); const w = tci.update((ap - e) / (0.015 * d)); wt1.set(w); wt2.set(signal.update(w)); }); ``` ### builtins/williams-fractals.ts ```ts // Williams Fractals (Bill Williams), as TradingView's built-in: a mark above each bar whose high is // above the `periods` bars either side of it, and below each bar whose low is below them. Known // `periods` bars later, drawn back on its own bar. Bars with equal highs or lows do not count. // Reads any bar type. indicator('Williams Fractals', { overlay: true, shortName: 'Fractals' }); const periods = input.int('Periods', 2, { min: 1, max: 50 }); const chart = bars(); const highs = ta.pivotHigh(periods, periods); const lows = ta.pivotLow(periods, periods); const up = plot.shape('Up fractal', { shape: 'triangleDown', location: 'absolute', color: color.down, size: 'small', offset: -periods }); const down = plot.shape('Down fractal', { shape: 'triangleUp', location: 'absolute', color: color.up, size: 'small', offset: -periods }); chart.onBar(() => { const h = highs.update(chart.high); const l = lows.update(chart.low); up.set(!isNa(h), { price: h }); down.set(!isNa(l), { price: l }); }); ``` ### builtins/williams-r.ts ```ts // Williams %R (Larry Williams, 1973), as TradingView's built-in: where the source sits in the range // of the last `length` bars' highs and lows, from 0 (at the high) to −100 (at the low). Bands at // −20 and −80. Reads any bar type: it needs the high and low. indicator('Williams %R', { shortName: '%R' }); const length = input.int('Length', 14, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const highest = ta.highest(length); const lowest = ta.lowest(length); const line = plot.line('%R', { color: color.study(13), range: { min: -100, max: 0 }, margins: { top: 0.08, bottom: 0.08 } }); const upper = plot.level('Upper band', -20, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); plot.level('Middle band', -50, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); const lower = plot.level('Lower band', -80, { style: 'dashed', color: color.withAlpha(color.neutral, 0.45) }); const background = plot.fill('Background', upper, lower, { color: color.withAlpha(color.study(13), 0.08) }); chart.onBar(() => { const top = highest.update(chart.high); const bottom = lowest.update(chart.low); line.set((100 * (chart.source(source) - top)) / (top - bottom)); }); ``` ### builtins/wma.ts ```ts // Weighted moving average, as TradingView's built-in WMA: the last `length` values weighted // 1 (oldest) to `length` (newest). The offset shifts the line by whole bars. Reads any bar type. indicator('WMA', { overlay: true, shortName: 'WMA' }); const length = input.int('Length', 9, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const offset = input.int('Offset', 0, { min: -500, max: 500 }); const chart = bars(); const wma = ta.wma(length); const line = plot.line('WMA', { color: color.study(11), offset }); chart.onBar(() => { line.set(wma.update(chart.source(source))); }); ``` ### builtins/woodies-cci.ts ```ts // Woodies CCI (Ken Wood), as TradingView's built-in: the 14-bar CCI of hlc3 as a histogram, green // above zero and red below, and the fast 6-bar "turbo" CCI as a line. Levels at ±100 and ±200. // Reads any bar type. indicator('Woodies CCI', { shortName: 'Woodies CCI' }); const turboLength = input.int('CCI turbo length', 6, { min: 1, max: 500 }); const length = input.int('CCI 14 length', 14, { min: 1, max: 500 }); const chart = bars(); const turbo = ta.cci(turboLength); const cci = ta.cci(length); const histogram = plot.histogram('CCI 14', { axisLabel: false }); const turboLine = plot.line('CCI turbo', { color: color.study(9), margins: { top: 0.1, bottom: 0.1 } }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.35) }); plot.level('+100', 100, { style: 'dashed', color: color.withAlpha(color.neutral, 0.35) }); plot.level('−100', -100, { style: 'dashed', color: color.withAlpha(color.neutral, 0.35) }); plot.level('+200', 200, { style: 'dotted', color: color.withAlpha(color.neutral, 0.25) }); plot.level('−200', -200, { style: 'dotted', color: color.withAlpha(color.neutral, 0.25) }); chart.onBar(() => { const price = chart.source('hlc3'); const c = cci.update(price); histogram.set(c, c >= 0 ? color.withAlpha(color.up, 0.6) : color.withAlpha(color.down, 0.6)); turboLine.set(turbo.update(price)); }); ``` ### builtins/zigzag.ts ```ts // Zig Zag: lines between the swing highs and lows that matter. A swing is a pivot, a high (or low) // with `depth` bars either side below (or above) it, so it is known `depth` bars after it formed. // A swing the other way from the last one counts only if it is at least `deviation` percent away // from it; a swing the same way that goes further replaces it. The newest leg runs on to the last // close. Drawn on the price pane as one line through the swings. indicator('Zig Zag', { overlay: true, shortName: 'ZigZag', maxPolylines: 1 }); const deviation = input.float('Price deviation for reversals (%)', 5, { min: 0.01, step: 0.5 }); const depth = input.int('Pivot legs', 10, { min: 1, max: 250 }); const extend = input.bool('Extend to last bar', true); const chart = bars(); const pivotHigh = ta.pivotHigh(depth, depth); const pivotLow = ta.pivotLow(depth, depth); // The swings, oldest first: bar numbers and prices. The last one may still be replaced. const swingBars = state.list(500); const swingPrices = state.list(500); // 1 when the last swing is a high, -1 a low, 0 before the first. const direction = state.bar(0); function swing(price: number, bar: number, high: boolean) { const n = swingPrices.length; const way = high ? 1 : -1; if (n === 0) { swingBars.push(bar); swingPrices.push(price); direction.value = way; return true; } const last = swingPrices.get(n - 1); if (direction.value === way) { if (high ? price > last : price < last) { swingBars.set(n - 1, bar); swingPrices.set(n - 1, price); return true; } return false; } if ((Math.abs(price - last) / last) * 100 < deviation) return false; if (n === 500) { swingBars.shift(); swingPrices.shift(); } swingBars.push(bar); swingPrices.push(price); direction.value = way; return true; } chart.onBar(() => { const h = pivotHigh.update(chart.high); const l = pivotLow.update(chart.low); let changed = false; if (!isNa(h)) changed = swing(h, chart.index - depth, true) || changed; if (!isNa(l)) changed = swing(l, chart.index - depth, false) || changed; if (!changed && !chart.isLast) return; const points: { bar: number; price: number }[] = []; for (let i = 0; i < swingPrices.length; i += 1) points.push({ bar: swingBars.get(i), price: swingPrices.get(i) }); if (extend && chart.isLast && points.length > 0 && points[points.length - 1].bar < chart.index) points.push({ bar: chart.index, price: chart.close }); if (points.length >= 2) draw.polyline({ points, color: color.study(9), width: 2 }); }); ``` ### builtins/zlema.ts ```ts // Zero-lag exponential moving average (John Ehlers and Ric Way): an EMA of the source plus its // change over the lag, (length − 1) / 2 bars, which cancels most of the EMA's delay. The EMA is // seeded like `ta.ema` once the lagged value exists. Reads any bar type. indicator('ZLEMA', { overlay: true, shortName: 'ZLEMA' }); const length = input.int('Length', 20, { min: 1, max: 500 }); const source = input.source('Source', 'close'); const lag = Math.floor((length - 1) / 2); const chart = bars(); const history = series(Math.max(1, lag)); const ema = ta.ema(length); const line = plot.line('ZLEMA', { color: color.study(8) }); chart.onBar(() => { const price = chart.source(source); history.set(price); line.set(ema.update(price + (price - history.ago(lag)))); }); ``` ### builtins/zscore.ts ```ts // Z-Score: how many standard deviations the source is from its `length`-bar simple average (the // population deviation, as ta.stdev). Levels at ±2. Reads any bar type. indicator('Z-Score', { shortName: 'Z' }); const length = input.int('Length', 20, { min: 2, max: 500 }); const source = input.source('Source', 'close'); const chart = bars(); const sma = ta.sma(length); const stdev = ta.stdev(length); const line = plot.line('Z-Score', { color: color.study(12), margins: { top: 0.1, bottom: 0.1 } }); plot.level('+2', 2, { style: 'dashed', color: color.withAlpha(color.neutral, 0.4) }); plot.level('Zero', 0, { style: 'dotted', color: color.withAlpha(color.neutral, 0.3) }); plot.level('−2', -2, { style: 'dashed', color: color.withAlpha(color.neutral, 0.4) }); chart.onBar(() => { const price = chart.source(source); line.set((price - sma.update(price)) / stdev.update(price)); }); ```