# VP Script — the indicator language for VerticalView charts

You are helping write an indicator in **VP Script**. It is NOT Pine Script. Read the rules below before writing code: the differences are the part that makes Pine habits produce scripts that do not compile.

Full documentation: https://0.0.0.0:3003/docs/script

## Rules that differ from Pine

- VP Script is TypeScript-shaped. Type annotations are allowed and are erased before it runs.
- It runs ONCE over the whole bar array — NOT once per bar. There is no `var`, no bar index advancing under you, and no evaluation order to hold in mind. `ta.*` are pure transforms from a whole series to a whole series.
- A series is `(number | null)[]`, one entry per bar. `null` means 'no value for this bar' — the warm-up bars of an average, a gap in a foreign feed. It is NOT zero.
- Never coalesce a null to zero to make arithmetic work. Use `math.add` / `math.sub` / `math.mul` / `math.div`, which propagate the gap. Plain `a + b` on a null gives NaN and draws as a break.
- There is no clock and no locale: `Date`, `Intl`, `Math.random` and `performance` are unavailable, and `Date`/`Intl` are refused at compile time. Read time from `bars[i].time`; format numbers with `toFixed`.
- For wall-clock time in a named zone use `time.*`: `time.minuteOfDay(t, 'America/New_York')`, `time.hour`, `time.minute`, `time.dayOfWeek`, `time.offsetMinutes`, `time.zoned` (all pieces at once) and `time.formatMinuteOfDay(570) === '09:30'`. This is the ONLY route to a zone, since `Intl` is refused — do not compute an offset by hand, it is wrong twice a year. An unknown zone throws rather than answering in UTC.
- A dashboard is `table(rows, { position: 'top-right', background, border, color, size })`, NOT a label full of newlines. Rows are arrays of cells; a cell is a string or `{ text, color, background, align, bold, size }`. Short rows are padded. It is pinned to a pane corner, so it does not slide away when the reader pans, and columns are measured and aligned for you. A label DOES break on newlines, but it is anchored to a bar and the font is proportional, so padded strings line nothing up.
- Every point takes `{ index, price }` OR `{ time, price }`, and `label` takes a bar index, `{ index }` or `{ time }` — this is Pine's `xloc.bar_time`. A timestamp resolves to a FRACTIONAL bar index, so a 09:30 edge lands at 09:30 on a 4h chart instead of snapping to the bar open, and a time past the last bar extrapolates so a session box can reach a close that has not happened yet. `barAt(t)` gives the number directly, or `null` when there is no bar duration to project against.
- `polyline(points, { color, width, closed, fillColor })` draws a curve through arbitrary points. Use it instead of `plot` for anything sampled from intrabar data: a plot is one value per BAR, and such a curve has many points inside one bar and none in the next. `line()` returns an id and `linefill(a, b, { color })` shades between two lines, the way `fill` shades between two plots.
- `input.*` takes `group` and `tooltip` on every kind, plus `step` on the numeric ones. A dense indicator without groups is a wall of nouns in the settings dialog. `input.timeframe(title, default)` is a resolution input that refuses a value the datafeed cannot read.
- `import`, `export`, `class`, `enum`, `namespace`, `async`, `await`, `yield`, `eval` and `Function` are all refused at compile time. A script is one self-contained synchronous file.
- `timeframe.period` is the chart's own resolution and `timeframe.compare(a, b)` orders two resolutions by duration. Pine's MTF indicators hide a layer finer than the chart — port that as `if (timeframe.compare(tf, timeframe.period) >= 0) { … }`. An unparseable resolution compares EQUAL, so it is shown rather than silently dropped.
- `request.security` is strictly causal: a foreign bar's value appears only from the chart bar that opened AFTER that foreign bar closed. There is no `lookahead_on` equivalent — do not try to emulate one.
- `request.security_lower_tf` returns one array per chart bar, and `null` (not `[]`) for a bar the finer feed does not FULLY span. `[]` means covered but nothing traded. Always handle the null branch.
- `alertcondition` fires on CLOSED bars only, at most once per bar, and never for history. Prefer an edge (`ta.crossover(a, b)`) over a state (`a > b`), or the alert fires on every bar of a trend.
- Drawing takes condition SERIES, not per-bar calls: `marker(condition, …)` and `bgcolor(condition, …)` draw wherever the series is truthy.
- `plot(series, { offset: 26 })` displaces the DRAWING only — the values stay on the bars that produced them, so alerts and fills are unaffected. Use this rather than `math.shift` for Ichimoku-style displacement.
- ALWAYS give plots and fills a `title`. The title is what the reader sees in the Style tab, where they can recolour, resize, restyle or hide each one individually — so you do NOT need to declare a colour input per line the way Pine ports often do. A reader's colour replaces a per-bar colour series.
- `label`, `line` and `box` take `offset` too, so an Ichimoku's Kumo labels can sit in the empty space right of the last bar. The anchor index is untouched; a shape is culled on where it is DRAWN, not where it is anchored.
- `request.security(sym, tf, compute, { warmupBars: 78 })` asks for foreign bars BEFORE the visible range. Use it for long-lookback studies — a monthly Ichimoku needs 52 for Senkou B plus 26 for displacement. It is a request, not a guarantee: still handle nulls.
- `timeframe.secondsOf(res)` converts ANY resolution to seconds, not just the chart's, so a script never needs its own table of resolution strings.
- There is NO equivalent of Pine's `lookahead_on`, and this is deliberate rather than missing. It exposes a higher-timeframe value before that bar closed, which draws one way on history and another live — repainting. Do not try to emulate it; write the causal version.
- `plot` and `fill` accept `color` as one colour OR one per bar (`(string | null)[]`), which is how a cloud changes colour where its edges cross. A fill inherits its plots' offset.
- Declare tunables with `input.*` so a reader changes them in the settings dialog instead of editing the code. Prefer `input.source` over hardcoding `close`.
- Loops are ordinary and carry an iteration budget; a runaway is stopped with an error naming the line.
- `log(...)` prints to the editor console while developing. It is inert for a published script, so leaving log calls in is harmless.

## A complete example

```ts
indicator('Two averages', { shortTitle: 'MAs' })

const fastLen = input.int('Fast', 10, { min: 2, max: 200 })
const slowLen = input.int('Slow', 30, { min: 2, max: 400 })
const src = input.source('Source', 'close')

const fast = ta.sma(src, fastLen)
const slow = ta.sma(src, slowLen)

plot(fast, { title: 'Fast', color: '#22c55e', width: 2 })
plot(slow, { title: 'Slow', color: '#ef4444', width: 2 })

marker(ta.crossover(fast, slow), { shape: 'triangleUp', color: '#22c55e', location: 'below' })
marker(ta.crossunder(fast, slow), { shape: 'triangleDown', color: '#ef4444', location: 'above' })
```

## Best practice

- Put `indicator(...)` first, then inputs, then computation, then drawing. Readers and reviewers both scan for that order.
- Use `pane: 'separate'` for anything not in price units, and `scale` to pin an oscillator's axis.
- Return gaps rather than guesses. If a value is not knowable on a bar, leave it null — a plot draws a gap, which is the honest answer.
- Name plots with `title` so the legend and the settings dialog are readable.
- Do not draw text that looks like an advertisement or a link; published scripts are checked for it and will be refused.
- An indicator must be deterministic: the same bars and inputs must always produce the same drawing, or it cannot pass review.
- You can share an unpublished indicator with one person by email; they accept it and it appears under 'Shared with me'. Publishing is only for making it public.

## Complete API reference

Everything callable. Nothing outside this list exists — if a function is not here, it is not available.

### Declaring the script

- `indicator(title, { shortTitle?, pane?, precision?, scale? })` — Names the script and says where it draws. `pane: 'separate'` puts it below the price instead of on it.

### The bars

- `open` — Open of every bar, as a series.
- `high` — High of every bar.
- `low` — Low of every bar.
- `close` — Close of every bar. The default source for most studies.
- `volume` — Volume of every bar. Zero or absent on instruments that do not report it.
- `hl2` — (high + low) / 2.
- `hlc3` — (high + low + close) / 3, the typical price.
- `ohlc4` — (open + high + low + close) / 4.
- `bars` — The raw bar objects, when a series is not enough.
- `barCount` — How many bars there are — `bars.length`, named for readability.
- `barAt(timeMs)` — Where a timestamp falls on the time axis, as a FRACTIONAL bar index — the same resolution a `{ time }` anchor uses. Past the last bar it extrapolates with the feed's own bar span, which is how a session box reaches its close before that bar exists. `null` when there is no bar duration to project against.

### Inputs

- `input.int(title, default, { min?, max?, step?, group?, tooltip? })` — A whole number — a lookback length.
- `input.float(title, default, { min?, max?, step?, group?, tooltip? })` — A decimal — a multiplier, a threshold. Give a `step`: the spinner defaults to free-form, and arrow keys on a 0-to-1 ratio want 0.05, not 1.
- `input.bool(title, default, { group?, tooltip? })` — A checkbox.
- `input.color(title, default, { group?, tooltip? })` — A colour, with an opacity slider attached.
- `input.string(title, default, { options?, group?, tooltip? })` — Free text, or a dropdown when `options` is given.
- `input.timeframe(title, default, { options?, group?, tooltip? })` — A resolution the reader can change — `'5'`, `'60'`, `'1D'`. Refuses a value the datafeed cannot read, naming the input, rather than letting it become a `request` nothing answers.
- `input.source(title, default?, { group?, tooltip? })` — Lets the reader choose which price series feeds the study.

### Drawing

- `plot(series, { title?, color?, width?, style?, lineStyle?, offset?, ignoreScale? })` — Draw a series as a line, histogram, area, stepline or circles. `color` takes one colour or one per bar; `offset` displaces the DRAWING only, leaving the values where they were computed. Returns an id `fill` can use.
- `hline(price, { color?, lineStyle?, title? })` — A horizontal line at a fixed price — a zero line, an overbought level.
- `fill(fromPlot, toPlot, { color?, offset? })` — Shade the band between two plots, or between two fixed prices. `color` takes one colour or one per bar, so a cloud can change colour where its edges cross; the displacement follows the plots unless you say otherwise.
- `marker(condition, { shape?, color?, location?, text? })` — A shape on every bar where the condition is truthy. The usual way to show a signal.
- `label(at, price, text, { color?, background?, size?, align?, offset? })` — Text anchored at one price. `at` is a bar index, `{ index }` or `{ time }`. Newlines break as lines. `offset` displaces the drawing only, so a label can sit in the empty space right of the last bar.
- `line(pointA, pointB, { color?, width?, lineStyle?, extend?, offset? })` — A segment between two points. Each point is `{ index, price }` or `{ time, price }`. Returns an id `linefill` can use. `offset` displaces the drawing only.
- `polyline(points, { color?, width?, lineStyle?, closed?, fillColor?, offset? })` — A curve through many points, each `{ index, price }` or `{ time, price }`. Use it where a `plot` cannot: a plot is one value per BAR, and a curve sampled from intrabar data has several points inside one bar and none in the next. A `fillColor` closes the path.
- `linefill(lineA, lineB, { color? })` — Shade the band between two lines, by the ids `line()` returned. The band is the quadrilateral between their endpoints, so two segments that do not span the same range give a wedge rather than a guess about where either continues.
- `box(cornerA, cornerB, { color?, fillColor?, text?, offset? })` — A rectangle between two corners, each `{ index, price }` or `{ time, price }` — a session range, a supply zone. `offset` displaces the drawing only.
- `table(rows, { position?, background?, border?, color?, size? })` — A dashboard pinned to a corner of the pane, below whatever chrome already covers it (the legend) — rows of cells, each a string or `{ text, color, background, align, bold, size }`. Columns are measured and lined up for you. Use this rather than a label full of newlines: a label is anchored to a bar, so it slides off screen when you pan, and its columns cannot line up in a proportional font.
- `bgcolor(condition, { color? })` — Tint the background of every bar where the condition is truthy.
- `barcolor(colors)` — Recolour the price bars themselves. One entry per bar; `null` keeps a bar's normal colour.
- `plotcandle(open, high, low, close, { upColor?, downColor?, wickColor?, title? })` — Draw your own OHLC series as candles — Heikin-Ashi, a smoothed series, another timeframe.

### Indicators and statistics

- `ta.sma(source, length)` — Simple moving average.
- `ta.ema(source, length)` — Exponential moving average.
- `ta.rma(source, length)` — Wilder's smoothing — what RSI and ATR are built on.
- `ta.wma(source, length)` — Linearly weighted moving average.
- `ta.stdev(source, length)` — Rolling standard deviation.
- `ta.rsi(source, length)` — Relative Strength Index, 0 to 100.
- `ta.change(source, length?)` — Difference from `length` bars ago (default 1).
- `ta.crossover(a, b)` — True on the bar where `a` crosses above `b`.
- `ta.crossunder(a, b)` — True on the bar where `a` crosses below `b`.
- `ta.cross(a, b)` — True on a cross in either direction.
- `ta.highest(source, length)` — Highest value over the last `length` bars.
- `ta.lowest(source, length)` — Lowest value over the last `length` bars.
- `ta.midline(length)` — Midpoint of the rolling high/low channel — the Donchian centre.
- `ta.atr(length)` — Average true range.
- `ta.tr()` — True range, unsmoothed.
- `ta.sum(source, length)` — Rolling sum.
- `ta.cum(source)` — Running total from the first bar.
- `ta.dev(source, length)` — Mean absolute deviation.
- `ta.median(source, length)` — Rolling median.
- `ta.rising(source, length)` — True when the series rose on each of the last `length` bars.
- `ta.falling(source, length)` — True when it fell on each of the last `length` bars.
- `ta.barssince(condition)` — Bars since the condition was last true. 0 on the bar it is true.
- `ta.valuewhen(condition, source, occurrence?)` — The value of `source` when the condition was last true.
- `ta.pivothigh(source, left, right)` — Pivot high, reported `right` bars later — when it first becomes knowable, not when it happened.
- `ta.pivotlow(source, left, right)` — Pivot low, on the same delayed basis.
- `ta.macd(source, fast?, slow?, signal?)` — Returns the MACD line, its signal and the histogram.
- `ta.bb(source, length?, mult?)` — Bollinger Bands: basis, upper and lower.
- `ta.kc(length?, mult?)` — Keltner Channels, from ATR rather than deviation.
- `ta.cci(length?)` — Commodity Channel Index.
- `ta.obv()` — On-balance volume.
- `ta.mfi(length?)` — Money Flow Index — RSI weighted by volume.
- `ta.roc(source, length)` — Rate of change, as a percentage.
- `ta.mom(source, length)` — Momentum — the raw difference.
- `ta.linreg(source, length, offset?)` — Linear regression value at the end of the window.
- `ta.percentrank(source, length)` — Where the current value sits in its own recent range, 0 to 100.
- `ta.correlation(a, b, length)` — Rolling correlation between two series.
- `ta.stoch(k?, d?, smooth?)` — Stochastic oscillator: %K and %D.
- `ta.supertrend(factor?, length?)` — SuperTrend line and its direction.
- `ta.hma(source, length)` — Hull moving average: weighted averages combined to reduce lag.
- `ta.swma(source)` — Symmetrically weighted four-bar moving average using the fixed 1, 2, 2, 1 kernel.
- `ta.vwma(source, length)` — Volume-weighted moving average. A bar without volume is a gap rather than a zero-weight observation.
- `ta.alma(source, length, offset?, sigma?)` — Arnaud Legoux moving average. `offset` defaults to 0.85 and `sigma` to 6.
- `ta.vwap(source?)` — Session-anchored volume-weighted average price. The source defaults to `hlc3`.
- `ta.dmi(diLength?, adxSmoothing?)` — Wilder directional movement: returns `{ plus, minus, adx }`. Both lengths default to 14.
- `ta.adx(diLength?, adxSmoothing?)` — Average Directional Index without the two directional-indicator series.
- `ta.sar(start?, increment?, maximum?)` — Parabolic SAR. Defaults are 0.02, 0.02 and 0.2.
- `ta.tsi(source, shortLength?, longLength?)` — True Strength Index. The short and long smoothing lengths default to 13 and 25.
- `ta.cmo(source, length)` — Chande Momentum Oscillator, from -100 to 100.
- `ta.wpr(length?)` — Williams %R, from -100 to 0. The length defaults to 14.
- `ta.bbw(source, length?, mult?)` — Bollinger bandwidth: the band's span as a fraction of its basis.
- `ta.variance(source, length)` — Rolling population variance.
- `ta.cog(source, length)` — Ehlers Centre of Gravity oscillator.
- `ta.highestbars(source, length)` — Bars back to the window's highest value. Returns 0 or a negative offset.
- `ta.lowestbars(source, length)` — Bars back to the window's lowest value. Returns 0 or a negative offset.
- `ta.percentile_linear_interpolation(source, length, percentage)` — Rolling percentile with linear interpolation between neighbouring values.
- `ta.percentile_nearest_rank(source, length, percentage)` — Rolling nearest-rank percentile; the answer is always a value that occurred in the window.
- `ta.mode(source, length)` — Most frequent value in the window. Ties resolve to the smallest value.

### Arithmetic that respects gaps

A series has holes, and `a + b` on a hole gives NaN — which draws as a break nobody asked for. These propagate the hole instead.

- `math.add(a, b)` — Add two series, or a series and a number.
- `math.sub(a, b)` — Subtract.
- `math.mul(a, b)` — Multiply.
- `math.div(a, b)` — Divide. Division by zero gives `NaN`, never Infinity.
- `math.abs(a)` — Absolute value.
- `math.min(a, b)` — Smaller of the two, bar by bar.
- `math.max(a, b)` — Larger of the two, bar by bar.
- `math.shift(a, n)` — Move a series forward (+) or back (−) by `n` bars.
- `math.pow(a, b)` — Raise each value in `a` to the corresponding power in `b`. Either input may be a number or series.
- `math.sqrt(a)` — Square root, bar by bar.
- `math.log(a)` — Natural logarithm, bar by bar.
- `math.log10(a)` — Base-10 logarithm, bar by bar.
- `math.exp(a)` — Exponential function, bar by bar.
- `math.floor(a)` — Round each value down to an integer.
- `math.ceil(a)` — Round each value up to an integer.
- `math.sign(a)` — Return -1, 0 or 1 for each value's sign.
- `math.round(a, decimals?)` — Round each value to `decimals` places. Negative `decimals` values round to tens, hundreds and beyond.
- `math.avg(a, b)` — Element-wise mean of two numbers or series.
- `math.clamp(a, low, high)` — Clamp each value into the inclusive range from `low` to `high`.

### The chart itself

Mostly for multi-timeframe studies: a layer finer than the chart is not a view of anything, and this is how a script declines to draw one.

- `timeframe.period` — The chart's own resolution, as the datafeed spells it — `'60'`, `'1D'`, `'1W'`.
- `timeframe.seconds` — How long one chart bar is, in seconds. `0` when the resolution cannot be parsed.
- `timeframe.secondsOf(resolution)` — Any resolution's length in seconds — `timeframe.secondsOf('1W')` — or `0` if it cannot be read. Saves carrying your own table of resolution strings.
- `timeframe.compare(a, b)` — `-1` if `a` is shorter than `b`, `1` if longer, `0` if equal — or if either cannot be read, so an unknown resolution never hides a layer.

### Time zones and sessions

Every one takes an epoch-millisecond time — a `bar.time`, or one from `request.security_lower_tf` — and an IANA zone name such as `America/New_York`. An unknown zone throws rather than answering in UTC: a session drawn five hours from where you put it looks entirely normal, and a refusal names the zone.

- `time.zoned(timeMs, zone)` — Every wall-clock piece at once — `{ year, month, day, hour, minute, second, weekday, minuteOfDay, offsetMinutes }`.
- `time.hour(timeMs, zone)` — The hour in that zone, 0–23.
- `time.minute(timeMs, zone)` — The minute past the hour.
- `time.minuteOfDay(timeMs, zone)` — `hour * 60 + minute` — the form a session window is written in. 09:30 New York is 570.
- `time.dayOfWeek(timeMs, zone)` — 0 = Sunday, as `Date` has it.
- `time.offsetMinutes(timeMs, zone)` — Minutes east of UTC at that instant, DST included. Subtract two to show one session's clock beside another's without a table that goes stale twice a year.
- `time.formatMinuteOfDay(minuteOfDay)` — `570` → `'09:30'`. Wraps past midnight in both directions.

### Trading sessions

- `session.isIn(timeMs, spec, zone?)` — Whether an instant is inside a trading session. The zone defaults to `syminfo.timezone`.
- `session.minutesInto(timeMs, spec, zone?)` — Minutes since the session opened, or `null` outside it. The zone defaults to `syminfo.timezone`.

### The instrument

- `syminfo` — The frozen instrument descriptor: `ticker`, `description`, `exchange`, `type`, `currency`, `mintick`, `pricescale`, `timezone` and `session`.

### Calculated colours

- `color.rgb(r, g, b, transp?)` — Build a colour from 0-to-255 channels. Pine transparency is used: 0 is opaque and 100 is invisible.
- `color.new(color, transp)` — Restate a colour at a new Pine-style transparency, replacing its existing alpha.
- `color.r(color)` — Red channel, from 0 to 255.
- `color.g(color)` — Green channel, from 0 to 255.
- `color.b(color)` — Blue channel, from 0 to 255.
- `color.t(color)` — Pine-style transparency, from 0 for opaque to 100 for invisible.
- `color.mix(from, to, weight)` — Blend colours and alpha. A weight of 0 returns `from`; 1 returns `to`.
- `color.fromGradient(value, low, high, lowColor, highColor)` — Map a clamped numeric range to a two-colour gradient.

### Number formatting

- `str.number(value, decimals?)` — A number with grouped thousands and a dot decimal separator. Decimals default to 2.
- `str.price(value, decimals?)` — A price using the instrument's quoted tick precision unless `decimals` is provided.
- `str.percent(value, decimals?, fraction?)` — A percentage string. Set `fraction` when 0.523 means 52.3 percent.
- `str.compact(value, decimals?)` — Compact large numbers with K, M, B or T suffixes.

### Other symbols and timeframes

- `request.security(symbol, resolution, compute?, { warmupBars? })` — A value from another symbol or timeframe, projected causally: a foreign bar appears only from the chart bar that opened after it closed. `warmupBars` asks for extra history BEFORE the visible range, which an indicator with a long lookback needs.
- `request.security_lower_tf(symbol, resolution)` — The finer bars INSIDE each chart bar, one array per bar. `null` where the finer feed does not fully cover the bar.

### Alerts

- `alertcondition(condition, { title, message? })` — Ask the host to watch a condition. Fires on closed bars only, at most once per bar, never for history.

### While you are writing

Development only. Capture is refused for any script the reader did not write, so an installed indicator's `log` calls do nothing on their chart and you never have to strip them before publishing.

- `log(...values)` — Print while you are writing. Shown in the editor's console for a script you wrote yourself, and captured for nobody else — a published indicator's `log` calls do nothing on a reader's chart, so you never have to strip them before publishing.

## When you answer

- Return one complete script, starting with `indicator(...)`.
- Use only functions from the reference above.
- Handle nulls with `math.*` rather than coalescing to zero.
- Do not use `Date`, `Intl`, `async`, `import`, `class` or `eval`.
