# Custom Indicators Reference — Sovereign Chart

> **For use with AI:** upload this file together with your PineScript code to
> Claude or ChatGPT and ask for the conversion. The ready-made prompt is at the
> end of the file.

---

## 1. Structure

An indicator is a single JavaScript file containing only two things:

```javascript
const meta = { /* indicator description */ };
function compute(candles, inputs) { /* the calculation */ }
```

No tabs, no `this._context`, no numeric indices for the inputs. That is all.

---

## 2. The `meta` object

```javascript
const meta = {
  name: 'RSI',                    // ≤16 chars — shown in the chart legend
  label: 'Relative Strength Index',
  color: '#4CAF50',               // main plot colour
  overlay: false,                 // true = over the candles | false = lower pane
  levels: [70, 30],               // horizontal reference lines (optional)
  levelAlpha: 0.15,               // their opacity (optional)
  showLastValue: false,           // hide the value label on the axis (optional)
  styleDefaults: { lineWidth: 2, lineStyle: 0 },  // 0=solid 1=dotted 2=dashed
  inputs: [
    { id: 'period', type: 'int', label: 'Period', def: 14, min: 2, max: 200, control: 'slider' },
    { id: 'mult',   type: 'float', label: 'Multiplier', def: 3.0, min: 0.1, max: 10, step: 0.1 },
    { id: 'fast',   type: 'int', label: 'Fast', def: 12, min: 1, max: 200, group: 'Fast / Slow' },
  ],
};
```

**`inputs` rules:**
- `id` must be a valid identifier (`[a-zA-Z_][a-zA-Z0-9_]*`) — it is used as a key in `inputs`
- `control: 'slider'` shows a slider; without it, a number field
- `section` starts a titled group · `inline` puts several parameters on one row
- `tooltip` explanatory text shown on hover
- The ceiling is 80 parameters

**The nine types:**

| `type` | value in `inputs` | UI | PineScript conversion |
|---|---|---|---|
| `int` / `float` | number | field or slider | `input.int` / `input.float` |
| `bool` | `true`/`false` | switch | `input.bool` |
| `color` | `'#2962FF'` | colour picker | `input.color` |
| `string` | text | text field | `input.string` |
| `select` | one of `options` | dropdown | `input.string(options=[...])` |
| `session` | `'0930-1100'` | two time pickers | `input.session` |
| `time` | `'0930'` | time picker | — |
| `timezone` | `'America/New_York'` | timezone list | `input.timezone` |
| `symbol` | `'US30'` or `''` | **the platform's symbol list** | `input.symbol` |
| `source` | `'close'` … or another indicator's output | source list | `input.source` |

> **`source`** — `compute` receives **an array of values**, not a string, and its
> list includes the outputs of other active indicators, so indicators can be
> stacked on one another. Details in section 3.6.
>
> ```javascript
> { id: 'src', type: 'source', label: 'Source', def: 'close' }
> ```

> **`symbol`** — do not hand-write a list of pairs in a `select`. This type opens
> the platform's own symbol list (with its search, categories and favourites)
> exactly as `input.symbol` does in TradingView, so the list stays correct
> whenever a pair is added. Its value is a symbol string passed straight to
> `request.security`, and **an empty `''` means "follow the chart's symbol"** — an
> option shown at the top of the list, so the user is never stuck on a fixed
> symbol.
>
> ```javascript
> { id: 'partner', type: 'symbol', label: 'Compared symbol', def: 'US30' }
> ```

```javascript
{ id: 'useAsia',   type: 'bool',    label: 'Asia',  def: true,        inline: 'asia', section: 'Sessions' },
{ id: 'asiaName',  type: 'string',  label: 'Name',  def: 'Asia',      inline: 'asia' },
{ id: 'asiaSess',  type: 'session', label: 'Time',  def: '2000-0000', inline: 'asia' },
{ id: 'asiaColor', type: 'color',   label: 'Colour',def: '#2962FF',   inline: 'asia' },
```

### The timezone — the session-time translator

Three separate time layers, exactly as in TradingView:

| Layer | Who sets it |
|---|---|
| The candle's stamp | The feed (Binance in UTC, MT5 in its server's time +GMT3 — corrected automatically) |
| **The indicator's timezone** | You: it is what the `session` string is interpreted in |
| The axis's timezone | The user, from the axis's gear — and it alone is what appears under the chart |

The difference between the last two is the offset the user sees: a session
written `1300-2200` in New York time is drawn at 20:00 on an axis set to
Baghdad. That is intended, not a bug.

The indicator's timezone is declared in one of two ways:

```javascript
var meta = {
  timezone: 'chart',            // a fixed declaration, or…
  inputs: [
    { id: 'zone', type: 'timezone', label: 'Timezone', def: 'America/New_York' }
  ]
};
```

The input takes precedence over the fixed declaration — the user has the last
word. The values: an IANA identifier (`'Europe/London'`), or `'UTC'`, or
`'chart'` (follows the axis's timezone, so what the user types is what they see)
or `'exchange'` (New York for forex, UTC for crypto).

> **Compatibility warning:** an indicator that does not declare its timezone
> keeps the old behaviour — New York by default, and no correction of the MT5
> feed's time. Adding `timezone` will shift its sessions away from what its users
> are accustomed to, so mention it when updating a published indicator.

Inside `compute` everything follows that timezone automatically:

```javascript
TA.inSession(c[i].time, S.ny)                  // in the indicator's timezone
TA.inSession(c[i].time, S.ny, 'Asia/Tokyo')    // an explicit timezone overrides it
TA.tz(c[i].time).h                             // the hour in the indicator's timezone
TA.tzName()                                    // the name of the timezone in force — for a chart label
```

> **An explicit argument is final** — just like the third argument of Pine's
> `time()`. An indicator that declares no `meta.timezone` follows the chart's
> axis timezone, but that is its **default** for a bare call only; it never
> overrides a timezone written into the call. Were it to override, every
> Pine-converted indicator's sessions would shift by the gap between the user's
> axis and the timezone written in the original.

Always-available zones: `UTC`, New York, Chicago, London, Berlin, Dubai, India,
Shanghai, Tokyo, Sydney — plus the indicator's timezone and the axis's timezone.
Anything else must be declared in `timezone`. The shorthands
`'NY'`/`'LDN'`/`'TOK'`/`'SYD'` are supported.

**`levels` rules:** either numbers `[70, 30]`, or objects `[{ value: 0, alpha: 0.1 }]`.

> Do not set an explicit `color` for levels unless you must — the default colour
> adapts to the light and dark themes automatically, an explicit one does not.

---

## 3. The `compute` function

```javascript
function compute(candles, inputs) {
  // candles: [{ time, open, high, low, close, volume }, ...]  in ascending order
  // inputs : { period: 14, mult: 3.0, ... }  the user's current values
  return [ /* plots */ ];
}
```

### The shape of a single plot

```javascript
{
  id: 'main',
  type: 'line',        // see "series types" below
  primary: true,       // the main plot — it takes the user's colour and width
  data: [{ time: 1700000000, value: 42.5 }, ...]
}
```

### Series types

**One value per candle** — `data: [{ time, value }]` (a gap = a point with no `value`):

| `type` | Form | Pine counterpart |
|---|---|---|
| `line` | a continuous line | `plot()` |
| `histogram` | bars (colour per bar, inside the point) | `plot(style=histogram/columns)` |
| `area` | a line with a gradient fill toward the bottom | `plot(style=area)` |
| `baseline` | two colours around a reference (`baseValue`) — green above / red below | `plot(style=baseline)` |
| `stepline` | a line that jumps with no diagonal — for fixed levels | `plot(style=stepline)` |

`baseline` needs a reference: `{ type: 'baseline', baseValue: 0, data: [...] }`.

**OHLC candles** — `data: [{ time, open, high, low, close }]` (optional
per-candle colour via `color`/`wickColor`/`borderColor`; a gap = a point with
`time` only):

| `type` | Form | Pine counterpart |
|---|---|---|
| `candle` | custom Japanese candles (Heikin Ashi, higher timeframe…) | `plotcandle()` |
| `bar`    | OHLC bars | `plotbar()` |

**Fill between two bounds (`band`)** — a cloud between two lines, the cleaner
replacement for `fill()`. It works on the main chart and in the lower pane alike:

```javascript
{ id: 'cloud', type: 'band', color: 'rgba(38,166,154,0.15)',
  data: [{ time: t, upper: 105, lower: 95 }, { time: t2 }, ...] }  // a point with no upper/lower = a gap
```

A Bollinger cloud example:

```javascript
const b = TA.bb(TA.src(candles), 20, 2);   // { basis, upper, lower, offset }
return [
  { id: 'basis', type: 'line', primary: true, data: TA.pt(candles, b.basis, b.offset) },
  { id: 'band',  type: 'band', color: 'rgba(90,200,250,0.12)',
    data: candles.map((c, i) => { const k = i - b.offset;
      return k >= 0 ? { time: c.time, upper: b.upper[k], lower: b.lower[k] } : { time: c.time }; }) },
];
```

**The candle's background (`bgcolor`)** — a full-height coloured column behind
the candle, the replacement for Pine's `bgcolor()`. Details and rules in
section 3.5:

```javascript
{ id: 'regime', type: 'bgcolor',
  data: [{ time: t, color: 'rgba(38,166,154,0.08)' }, ...] }   // a candle with no colour: leave it out
```

**A secondary plot with a fixed style** (unaffected by the user's settings):

```javascript
{
  id: 'signal', type: 'line',
  style: { color: '#FF9800', lineWidth: 1, lineStyle: 0, title: '' },
  data: [...]
}
```

**Colouring each bar individually** (for a histogram) — the colour goes inside
the data point:

```javascript
{ id: 'hist', type: 'histogram',
  data: [{ time: t, value: v, color: v >= 0 ? '#26a69a' : '#ef5350' }, ...] }
```

**Gaps and diagonal lines:** a point with no `value` = a gap. The line connects
points that have values, skipping over gaps — this is how zigzag patterns and
highs/lows are drawn.

```javascript
data.push({ time: t });              // a gap
data.push({ time: t, value: 100 });  // a point
```

---

## 3.1 Shapes — boxes, lines and text

The "one value per candle" model cannot draw a rectangle spanning candle 50 to
candle 90, nor write text at a given price. Hence a third output type:

```javascript
{ id: 'zones', type: 'shapes', shapes: [ /* … */ ] }
```

Coordinates are **(unix time in seconds, price)** — the same `time` that is in
the candles.

> **`xloc: 'bar_index'` — bar-number coordinates** (the counterpart of Pine's `xloc.bar_index`)
>
> Add `xloc: 'bar_index'` to the shape and every horizontal coordinate in it
> becomes a **bar number** rather than a time:
>
> ```javascript
> { kind: 'line', xloc: 'bar_index', x1: i, x2: i + 20, y1: p1, y2: p2 }
> ```
>
> It applies to `box`, `line`, `label`, `circle`, `fill` (the `t` field) and
> `linefill`. Numbers outside the range are projected by the timeframe's step
> (`n + 5` = five bars past the last, negatives before the first) — but future
> times remain outside the time scale as explained in the projection note below.
> Without `xloc`, `x` stays a literal time, so no existing indicator changes.

### `box` — a rectangle

```javascript
{ kind: 'box',
  x1: startTime, y1: topPrice, x2: endTime, y2: bottomPrice,
  bgColor: 'rgba(41,98,255,0.3)',   // the fill
  borderColor: '#2962FF',
  borderWidth: 1,
  style: 'solid',                    // 'solid' | 'dotted' | 'dashed'
  text: 'Asia',                      // text in the middle of the box (optional)
  textColor: '#2962FF',
  font: 'mono' }                     // 'sans' (default) | 'mono' | 'serif'
```

### `line` — a line between two points

```javascript
{ kind: 'line',
  x1: t1, y1: p1, x2: t2, y2: p2,
  color: '#26a69a',
  width: 1,
  style: 'dotted',
  extend: 'right' }                  // 'none' | 'left' | 'right' | 'both'
```

A diagonal line is drawn by making `y1 ≠ y2` — this is how trend lines and
high-to-low connections are drawn.

> **Future projection:** `extend: 'right'` extends the line to the pane's edge
> **preserving its slope** (for trend lines and forecasts). Define the line by
> two real points inside the data and then extend it — do not put an endpoint at
> a future time (past the last candle), because future times are outside the time
> scale and get clipped to the edge with no slope. To project to a specific
> future point, a series (candles / a forecast line) must occupy those times so
> that they enter the axis.

### `label` — text at a point

```javascript
{ kind: 'label',
  x: time, y: price,
  text: 'NYAM.H',
  textColor: '#089981',
  bgColor: 'rgba(0,0,0,0.6)',        // a background for the text (optional)
  size: 11,                          // 7 – 28
  position: 'above',                 // 'above' | 'below' | 'middle'
  align: 'left',                     // 'left' | 'center' | 'right'
  font: 'mono',                      // 'sans' (default) | 'mono' | 'serif'
  bold: true }
```

#### Label styles — `style` (the counterpart of `label.style_*`)

Without a `style` (or with `style: 'none'`) the behaviour above holds: free text
at the point. With a `label_*` style, **a card with a triangular pointer aimed at
the point** is drawn — the familiar Pine label:

```javascript
{ kind: 'label',
  x: time, y: price,
  text: 'CHoCH',
  style: 'label_right',              // the card sits left of the point, the pointer aims right
  bgColor: '#2962FF',                // the card's colour (blue by default)
  textColor: '#ffffff',
  borderColor: '#ffffff' }           // an optional border
```

| `style` | Card position | Pine counterpart |
|---|---|---|
| `label_right` | left of the point | `label.style_label_right` |
| `label_left` | right of it | `label.style_label_left` |
| `label_up` | below it | `label.style_label_up` |
| `label_down` | above it | `label.style_label_down` |
| `label_center` | exactly on it (no pointer) | `label.style_label_center` |
| `label_upper_left` / `label_upper_right` | diagonally above | the same names |
| `label_lower_left` / `label_lower_right` | diagonally below | the same names |

Inside the card `align` centres the text by default, and `position`
(above/below) has no effect — the style is what decides the position. An unknown
style falls back to `'none'` rather than the shape being rejected.

### `polyline` — a free path and polygon (replacement for `polyline.new`)

`box` is axis-aligned and `fill` needs two bounds at the same time — so neither
can draw a triangle nor a slanted quadrilateral. This shape is a single path
across N points, filled or stroked or both, and it is what "3D" profiles,
triangles and solid zigzag paths are built on.

```javascript
{ kind: 'polyline',
  points: [ { x: t1, y: p1 }, { x: t2, y: p2 }, { x: t3, y: p3 } ],
  color: '#2962FF',                  // the border colour ('width: 0' = no border)
  width: 1,
  style: 'solid',                    // 'solid' | 'dotted' | 'dashed'
  fillColor: 'rgba(41,98,255,0.25)', // optional — without it, a path with no fill
  closed: true,                      // closes the last edge (required for a polygon)
  curved: false }                    // quadratic smoothing (the counterpart of curved=true)
```

- **The limit is 2,000 points** per polyline; and a path is a single draw call
  however long it is, so a 500-point polygon is cheaper than 500 boxes.
- A point whose coordinate is outside the data's range is **skipped** and does
  not break the path — breaking it would have opened the polygon and let its
  fill leak.
- `xloc: 'bar_index'` applies to `x` in every point.
- A polygon with a single valid point is dropped (there is no path from one point).

### `circle` — a round point

```javascript
{ kind: 'circle',
  x: time, y: price,
  radius: 4,                         // 1 – 30 pixels
  color: '#26a69a',                  // the fill
  borderColor: '#ffffff',            // optional
  borderWidth: 1 }
```

### `fill` — a fill between two bounds (a cloud)

The replacement for PineScript's `fill()`. One shape carries a series of points,
each of which defines the upper and lower bound at a given time.

```javascript
{ kind: 'fill',
  color: 'rgba(90,200,250,0.12)',
  points: [
    { t: time1, top: 105, bottom: 95 },
    { t: time2 },                     // a point with no top/bottom = a gap that breaks the fill
    { t: time3, top: 108, bottom: 97 },
  ] }
```

The gap ends the current segment and starts another — use it during the warm-up
period, otherwise the fill stretches across the void. The maximum is
**60,000 points** per fill.

### `linefill` — a fill between two sloped lines (replacement for `linefill.new`)

`fill` needs a series of points aligned with the candles, which suits clouds and
bands. For a fill between **two arbitrary lines** — a channel, a fan, a triangle
— use this:

```javascript
{ kind: 'linefill',
  color: 'rgba(41,98,255,0.15)',
  extend: 'right',                   // applies to both lines
  line1: { x1: t1, y1: p1, x2: t2, y2: p2 },
  line2: { x1: t1, y1: p3, x2: t2, y2: p4 } }
```

- `a`/`b` are synonyms for `line1`/`line2`.
- Each line may carry its own `extend`; otherwise it inherits the shape's.
- The two lines are **not drawn** — add two `line` shapes if you want visible borders.
- `xloc: 'bar_index'` applies to all four coordinates.
- A line missing an endpoint drops the whole fill (there is no half polygon).

### `panel` — a pinned stats panel (replacement for `table.new`)

The only shape here that is tied to **neither** time nor price: it is positioned
in pixels from the chart's edge, so it does not move with scrolling or zooming.

```javascript
{ kind: 'panel',
  position: 'top_right',             // top/middle/bottom × left/center/right
  bgColor: 'rgba(19,23,34,0.85)',
  borderColor: '#2a2e39',
  borderWidth: 1,
  textColor: '#d1d4dc',              // the default text colour for every cell
  size: 11,                          // the default font size (7 – 28)
  width: 25,                         // optional: % of the chart's width for the whole panel
  height: 12,                        // optional: % of its height
  rows: [
    // a plain text cell, or an object with its properties
    ['Trend', { text: 'Up', align: 'right', textColor: '#26a69a' }],
    [{ text: 'RSI', bold: true }, { text: '62.4', align: 'right' }],
  ] }
```

Cell properties: `text`, `textColor`, `bgColor`, `align`, `size`, `bold`,
`font`, and `width`/`height` as a percentage of the chart (the counterpart of
`table.cell(width=, height=)`). Without a percentage the column is measured from
its widest text. `font` on the panel applies to all of its cells, and on a cell
it takes precedence — and `'mono'` is what makes columns of numbers line up.

> **Panel rules:**
> - **4 panels** per indicator at most, **24 rows × 8 columns**, and **64
>   characters** per cell. They count against the 1,500-shape ceiling by their
>   cell count, not as one shape.
> - Panels from several indicators in the **same position stack automatically**
>   and never overlap; their order is stable and does not change from frame to
>   frame. Bottom panels stack upwards.
> - They are always drawn above everything, and never affect the price scale.
> - Text wider than its cell is clipped at the cell's edge and never creeps onto
>   its neighbour.

> **Shape constraints:**
> - They work on the main chart (`overlay: true`) **and in the lower pane**
>   (`overlay: false`) alike — the coordinates are prices, so a pure-shapes
>   indicator in its own pane sets its `y` values within its own range.
> - The maximum is **1,500 shapes**. Honour a parameter such as `maxDays` rather
>   than drawing the whole history.
> - Shapes are not interactive and are removed automatically with the indicator.

### Drawing in a separate sub-pane (stacked rows) — read this before converting any "pane" indicator

Many indicators (ICT Quarterly, session maps, gauges under the chart) draw in a
**separate pane** as coloured rows rather than on the price. To convert them
correctly:

1. **`overlay: false`** — this alone routes the indicator to the lower pane.
   `overlay: true` always draws on the candles, whatever else you do.
2. **The `y` coordinate = a fixed row number, not a price.** Give each
   cycle/row a number: the first `[0,1]`, the second `[1,2]`… and the axis adapts
   automatically to the number of rows. **Do not use `high`/`low` (price) in a
   sub-pane** — it will stretch the scale (to 0..1860, say) and render it
   meaningless.
3. `box`, `label`, `band`, `bgcolor` and `markers` all work in the sub-pane.
   Markers there are built as shapes and attached to the **primary plot's value**
   at that candle rather than to the candle (there are no candles in the pane),
   so `aboveBar` means "above the line". Therefore: a marker at a time where the
   primary plot has no value is dropped — except `atPrice*`, which carries its
   own explicit price.

> **A decisive architectural constraint: one indicator = one pane.** If the
> source draws some elements **on the price** and others **in a separate pane at
> the same time** (such as a `Display on chart` switch per cycle), that is
> **impossible in a single indicator** here. Pick one of the two modes (usually
> the row-based pane), **drop the dual switch** rather than leaving it as a dead
> option, and mention it in a comment.

```javascript
// a row per enabled cycle — no price
let rows = 0; const R = {};
if (inputs.showDaily)  R.daily  = ++rows;
if (inputs.showNinety) R.ninety = ++rows;
if (inputs.showMicro)  R.micro  = ++rows;

// the quarter's box in its row, coloured by quarter:
shapes.push({ kind: 'box', x1: t1, x2: t2, y1: R.daily, y2: R.daily - 1,
  bgColor: 'rgba(139,188,252,0.35)', borderColor: '#8bbcfc' });
shapes.push({ kind: 'label', x: t1, y: R.daily - 0.5, text: 'Q3',
  textColor: '#8bbcfc', size: 9, position: 'middle' });
```

---

## 3.2 Markers — the replacement for `plotshape`

Arrows and triangles above and below the candles, for entry and exit signals:

```javascript
{ id: 'signals', type: 'markers', markers: [
    { time: t,
      position: 'belowBar',        // 'aboveBar' | 'belowBar' | 'inBar'
      shape: 'arrowUp',            // 'arrowUp' | 'arrowDown' | 'circle' | 'square'
      color: '#26a69a',
      text: 'BUY',                 // optional, ≤24 chars
      size: 1 },                   // 0.1 – 5
]}
```

To place one at a specific price rather than tying it to the candle, use
`atPriceTop`, `atPriceBottom` or `atPriceMiddle` — these **require** a `price`
field:

```javascript
{ time: t, position: 'atPriceMiddle', shape: 'circle', color: '#fff', price: 65400 }
```

> The maximum is **2,000 markers**, and there is no need to sort them by time —
> that happens automatically.
>
> They work on the main chart and in the lower pane alike. The difference is that
> the main chart places them above/below the **candle** (the library's own
> feature), while the lower pane places them above/below the **primary plot's
> value** — see rule 3 in the previous section.

The PineScript shape conversion table:

| PineScript | here |
|---|---|
| `shape.triangleup` / `shape.arrowup` | `shape: 'arrowUp'` |
| `shape.triangledown` / `shape.arrowdown` | `shape: 'arrowDown'` |
| `shape.circle` | `shape: 'circle'` |
| `shape.square` / `shape.diamond` / `shape.flag` | `shape: 'square'` |
| `location.abovebar` / `location.belowbar` | `position: 'aboveBar'` / `'belowBar'` |
| `location.absolute` | `position: 'atPriceMiddle'` + `price` |

Shapes and series can be combined in the same indicator:

```javascript
return [
  { id: 'ma',    type: 'line',   primary: true, data: TA.pt(candles, ma, 19) },
  { id: 'zones', type: 'shapes', shapes: boxes },
];
```

---

## 3.3 The symbol and the timeframe — `syminfo` and `timeframe`

Two globals inside `compute` (not passed as arguments, exactly as in
PineScript):

```javascript
syminfo.ticker          // 'BTCUSDT'
syminfo.tickerid        // the same

timeframe.period        // 'M15' — in the platform's notation
timeframe.pine          // '15'  — its Pine counterpart, so code ports literally
timeframe.in_seconds    // 900
timeframe.multiplier    // 15
timeframe.isintraday    // true
timeframe.isseconds / isminutes / isdaily / isweekly / ismonthly
```

The `timeframe.pine` mapping: `M15 → '15'`, `H4 → '240'`, `D1 → 'D'`,
`W1 → 'W'`, `MN1 → 'M'` — so a `timeframe.pine == '240'` condition ported from
Pine works as is.

> **They update automatically** when the symbol or the timeframe changes, and the
> indicator is recalculated then.
>
> **They may come back empty** on the validation path before a symbol has loaded
> (as in the editor's test on synthetic candles) — do not assume a value:
> `syminfo.ticker || '—'`.

---

## 3.4 Data from another symbol or timeframe — `request.security`

```javascript
const htf = request.security('', 'H4');        // same symbol, four-hour timeframe
const btc = request.security('BTCUSDT', 'D1'); // another symbol
const h4  = request.security('H4');            // shorthand: a single argument = the timeframe
```

It returns **an array of candles** `[{ time, open, high, low, close, volume }]`
in ascending order — then calculate on it with the `TA` library like any
candles, and project the result onto the current timeframe with `request.map`:

```javascript
function compute(candles, inputs) {
  const d1 = request.security('', 'D1');
  if (!d1.length) return [{ id: 'ema', type: 'line', data: TA.plot(candles, candles.map(() => NaN)) }];

  const ema = TA.ema(TA.src(d1), 20);          // 19 shorter than d1
  const full = TA.align(d1, ema, 19);          // ← the length of d1
  const onChart = request.map(candles, d1, full);   // ← the length of the current candles
  return [{ id: 'ema', type: 'line', data: TA.plot(candles, onChart) }];
}
```

| Function | Description |
|---|---|
| `request.security(sym, tf)` | the requested symbol's/timeframe's candles (empty if they have not arrived yet) |
| `request.ready(sym, tf)` | have they arrived? to tell that apart from "no data" |
| `request.map(candles, src, vals, opts)` | projects the source's series onto the current candles |
| `request.step(src)` | the source candle's duration in seconds |

**Notation:** an empty symbol = the current symbol, and an empty timeframe = the
chart's timeframe. The timeframe is accepted in the platform's notation (`'H4'`,
`'D1'`, `'MN1'`) or in Pine's (`'240'`, `'D'`, `'3M'`), and
`'BINANCE:BTCUSDT'` is read as a valid symbol.

**Available symbols** — nothing else is ever fetched (indicator code is shared,
so it does not get to decide what is requested from the network):

| Market | Symbols |
|---|---|
| Crypto spot | `BTCUSDT` `ETHUSDT` `SOLUSDT` `XRPUSDT` `BNBUSDT` `ADAUSDT` `DOGEUSDT` `SHIBUSDT` `DOTUSDT` `LTCUSDT` `LINKUSDT` |
| Crypto futures | the same with an `f` suffix — `BTCUSDTf` … |
| Indices and commodities | `US500` `US30` `US100` `WTI` `BRENT` `XAUUSD` `XAGUSD` |
| Forex | `EURUSD` `GBPUSD` `USDJPY` `USDCAD` `USDCHF` `EURJPY` `GBPJPY` `EURAUD` `EURCAD` `EURNZD` `GBPAUD` `GBPCAD` `GBPNZD` |

> Our mapping for the US futures: **ES → `US500`**, **YM → `US30`**,
> **NQ → `US100`** — these are what SMT indicators ported from TradingView use.

### Comparing two symbols on the **same** timeframe (SMT and the like)

Here the second rule flips: `request.map` without `lookahead` returns the last
**closed** candle, and on the same timeframe that is the **previous** candle — so
the comparison shifts by a whole candle. Comparing two synchronous symbols needs
`{ lookahead: true }`, and that is not lookahead bias: both candles close at the
same instant.

```javascript
const meta = {
  name: 'SMT', label: 'SMT Divergence', overlay: true,
  inputs: [
    // the `symbol` type opens the platform's symbol list — do not hand-write a list of pairs
    { id: 'partner', type: 'symbol', label: 'Compared symbol', def: 'US30' },
    { id: 'left',  type: 'int', label: 'Left',  def: 3, min: 1, max: 20 },
    { id: 'right', type: 'int', label: 'Right', def: 3, min: 1, max: 20 },
  ],
};

function compute(candles, inputs) {
  const other = request.security(inputs.partner, '');     // the same timeframe
  const shapes = [];
  if (other.length) {                                     // ← the first run returns []
    const oHigh = request.map(candles, other, other.map(c => c.high), { lookahead: true });
    const ph = TA.pivothigh(TA.src(candles, 'high'), inputs.left, inputs.right);
    let prev = -1;
    for (let i = 0; i < candles.length; i++) {
      if (!isFinite(ph[i])) continue;
      if (prev >= 0 && isFinite(oHigh[i]) && isFinite(oHigh[prev])) {
        const mine = ph[i] > ph[prev], theirs = oHigh[i] > oHigh[prev];
        // a higher high here and a lower high there (or the reverse) = divergence
        if (mine !== theirs) shapes.push({
          kind: 'label', x: candles[i].time, y: ph[i], text: 'SMT',
          style: 'label_down', bgColor: mine ? '#ef5350' : '#26a69a',
        });
      }
      prev = i;
    }
  }
  return [{ id: 'smt', type: 'shapes', shapes }];
}
```

> ### The two rules that are never broken
>
> **1. The first run returns empty — always.** The sandbox has no network and no
> waiting: `request.security` registers the request and returns an empty array,
> the host fetches it in the background and then **recalculates the indicator**,
> at which point it arrives. Write code that tolerates `[]` (as in the example
> above), otherwise it flashes an error on every activation and symbol switch.
>
> **2. `request.map` without `lookahead` does not repaint.** The default is that
> the current candle's value is the value of the source's last **closed** candle
> — that is, what was actually known at the time. `{ lookahead: true }` reveals
> the in-progress candle, making history look more accurate than was possible:
> valid for display, not for an entry signal.

**Limits:** four series per indicator (the fifth always returns empty), a
thousand candles per series, refreshed every minute. Series from another market
are shifted automatically to the chart's time convention (the MT5 feed leads UTC
by three hours) so they land on their correct candles. And in a **backtest
session** the series is truncated at the replay's clock, so the indicator never
sees a future that was not known.

> **In the editor** the run button waits once for the data to arrive so it can
> show you a complete result; if it does not arrive, a line in the debug panel
> names what was missing. And the publishing gate judges the code on synthetic
> candles with old dates — so the projection may come out entirely `NaN` there;
> return a series even if it has no values so that it passes.

---

## 3.5 Colouring the candle's background — the replacement for `bgcolor`

A full-height coloured column behind the candle. It is what "trend" and "market
regime" indicators need: green when the regime is bullish, red when it is
bearish, no colour when neutral.

```javascript
{ id: 'regime', type: 'bgcolor', data: [
    { time: t, color: 'rgba(38,166,154,0.08)' },
    // a candle that is not coloured: do not include it at all — there is no colourless point
] }
```

A complete example inside `compute`:

```javascript
const ema = TA.align(candles, TA.ema(TA.src(candles, 'close'), inputs.len), inputs.len - 1);
const bg = [];
for (let i = 0; i < candles.length; i++) {
  if (!isFinite(ema[i])) continue;                       // no colour during the warm-up
  bg.push({ time: candles[i].time,
            color: candles[i].close > ema[i] ? 'rgba(38,166,154,0.10)'
                                             : 'rgba(239,83,80,0.10)' });
}
return [
  { id: 'ema', type: 'line', primary: true, data: TA.pt(candles, ema) },
  { id: 'bg',  type: 'bgcolor', data: bg },
];
```

> **Background rules:**
> - It is drawn **beneath everything** — beneath the indicator's own shapes and
>   beneath the candles.
> - It **never affects the price scale**: it has no price at all, it spans from
>   the top of the pane to the bottom.
> - Adjacent candles of the same colour are **merged automatically** into one
>   column, so do not worry about colouring the whole history: 3,000 candles in
>   two colours become dozens of columns, not 3,000 shapes. (And they do not count
>   against the 1,500-shape ceiling — they are not shapes.)
> - Use **very transparent** colours (0.05–0.15). A solid colour swallows the
>   candles.
> - It works on the main chart and in the lower pane alike.

---

## 3.6 The price source — `input.source`

An input with which the user chooses what the indicator feeds on. The difference
from `select`: the value reaches `compute` as **a ready array of values** the
length of the candles, exactly as in PineScript — not a string you translate
yourself.

```javascript
var meta = {
  name: 'MYRSI', label: 'RSI with a source', overlay: false, color: '#5ac8fa',
  inputs: [
    { id: 'len', type: 'int', label: 'Length', def: 14, min: 2, max: 200 },
    { id: 'src', type: 'source', label: 'Source', def: 'close' },
  ],
};

function compute(candles, inputs) {
  // inputs.src is an array of numbers the length of candles — not 'close' nor 'hlc3'
  const r = TA.rsi(inputs.src, inputs.len);
  return [{ id: 'main', type: 'line', primary: true,
            data: TA.pt(candles, TA.align(candles, r, inputs.len)) }];
}
```

The list the user sees:

| Group | Options |
|---|---|
| The candle's prices | `close` `open` `high` `low` `hl2` `hlc3` `ohlc4` `hlcc4` |
| **The active indicators' outputs** | every plot of every other indicator currently enabled on the chart |

The second group is **indicator stacking**: an RSI on a moving average, a moving
average on CVD, Bollinger on your own custom indicator's output. Write nothing
for it — declaring one `source` input is enough, and the rest is the user's
choice from the settings panel.

> **Source rules:**
> - `TA.src(candles, 'hl2')` still exists for anyone who wants a fixed source in
>   the code with no input. The input is what makes it **the user's choice**.
> - **Write code that tolerates `NaN`.** Another indicator's output starts with a
>   warm-up period, and the feeding indicator may be switched off so the whole
>   series arrives as `NaN`. Do not throw — return a series even if it has no
>   values.
> - **One draw cycle of lag.** The source is the last thing actually drawn for the
>   feeding indicator, not a recalculation of it. Its effect is confined to the
>   last candle during a live tick, and the two cycles converge immediately.
>   (This is what prevents circular recursion: A reads B reads A.)
> - **Switching the symbol or the timeframe is safe**: alignment is by time, not
>   by index, so the old market's data does not match the new candles' times and
>   comes out as a gap rather than a false value.
> - The built-in indicators (SMA, RSI and so on) keep their old string source —
>   indicator stacking is a custom-indicator feature.

---

## 4. The `TA` library

Every function is available globally with no import.

### Conversion

| Function | Description |
|---|---|
| `TA.src(candles, 'close')` | extracts an array of prices (`open`/`high`/`low`/`close`/`volume`) |
| `TA.pt(candles, values, offset)` | **important** — binds an array of values to the candles' times |
| `TA.hl2(c)` `TA.hlc3(c)` `TA.ohlc4(c)` | price averages |

**`TA.pt` and `offset`:** windowed functions return an array shorter than the
candles. `offset` is the index of the candle matching the first value.

```javascript
const closes = TA.src(candles);
const ma = TA.sma(closes, 20);        // length = candles.length - 19
TA.pt(candles, ma, 19);               // ← 19 = 20 - 1
```

The offset rule: `sma/ema/stdev/highest/lowest` ← `n - 1` · `rma/rsi` ← `n` · `tr` ← `1`

### Averages and measures

| Function | Note |
|---|---|
| `TA.sma(values, n)` | length: `len - n + 1` |
| `TA.ema(values, n)` | length: `len - n + 1` |
| `TA.rma(values, n)` | Wilder's average — length: `len - n + 1` |
| `TA.stdev(values, n)` | standard deviation (biased) |
| `TA.highest(values, n)` / `TA.lowest(values, n)` | the high/low over `n` |
| `TA.change(values)` | the difference from the previous — same length, first element `0` |
| `TA.tr(candles)` | true range — length: `len - 1` (the `ta.tr` equivalent) |
| `TA.tr(candles, true)` | full length, bar 0 = `high - low` (the `ta.tr(true)` equivalent) |
| `TA.rsi(values, n)` | ready-made — length: `len - n` |

### The extended library

Two conventions, no third:

- **Windowed functions** return a shorter array with a known offset (like `sma`)
  — use them with `TA.pt(candles, out, offset)` or `TA.align`.
- **Signal helpers** (`crossover`, `rising`, `barssince`, `pivot*`, `valuewhen`)
  work on arrays **the same length as the candles** — lift any short array to
  that with `TA.align`.

**Averages:**

| Function | Offset |
|---|---|
| `TA.wma(v, n)` | `n - 1` — linearly weighted average |
| `TA.hma(v, n)` | `(n-1) + (⌊√n⌋-1)` — Hull |
| `TA.vwma(candles, n)` | `n - 1` — volume weighted |
| `TA.swma(v)` | `3` — weights `[1,2,2,1]/6` |
| `TA.linreg(v, n)` | `n - 1` — linear regression value |

**Measures and ranges:**

| Function | Offset / note |
|---|---|
| `TA.atr(candles, n)` | **`n - 1`** — matches `ta.atr`: `TA.rma(TA.tr(c, true), n)` |
| `TA.sum(v, n)` | `n - 1` — rolling sum |
| `TA.cum(v)` | full length — cumulative sum |
| `TA.roc(v, n)` / `TA.mom(v, n)` | `n` — relative / absolute change |
| `TA.dev(v, n)` | `n - 1` — mean absolute deviation |
| `TA.variance(v, n)` | `n - 1` |
| `TA.percentrank(v, n)` | `n` |
| `TA.correlation(a, b, n)` | `n - 1` — `a,b` of one length |

**Oscillators:**

| Function | Offset |
|---|---|
| `TA.cci(candles, n)` | `n - 1` |
| `TA.wpr(candles, n)` | `n - 1` — Williams %R |
| `TA.stoch(candles, n)` | `n - 1` — raw %K (smooth it with `sma` for %D) |
| `TA.cmo(v, n)` | `n` — Chande Momentum |
| `TA.mfi(candles, n)` | `n` — Money Flow Index |

**Bundles — they return an object with one `offset` shared by all of its arrays:**

```javascript
const m = TA.macd(close, 12, 26, 9);   // { macd, signal, hist, offset } — offset is slow-1
TA.pt(candles, m.hist, m.offset);      // plot it with the shared offset
const band = TA.bb(close, 20, 2);      // { basis, upper, lower, offset }
TA.dc(candles, 20);                    // Donchian { upper, lower, basis, offset }
TA.kc(candles, 20, 2);                 // Keltner  { basis, upper, lower, offset }
TA.vwap(candles);                      // full length — resets every NY day
```

> The shared offset is that of the **earliest** array in the bundle; arrays that
> start later carry `NaN` at their front — exactly where Pine has `na`. In
> `TA.macd` every array has offset `slow - 1`, and `signal`/`hist` stay `NaN`
> for a further `sig - 1` bars. Do not trim that front yourself: `TA.pt` and
> `TA.plot` turn `NaN` into a gap.

**Signal helpers (full length):**

| Function | Description |
|---|---|
| `TA.align(candles, short, offset)` | a short array ← the length of the candles (`NaN` during warm-up) |
| `TA.plot(candles, full)` | a full array ← plot points (a gap at `NaN`) |
| `TA.crossover(a, b)` / `TA.crossunder(a, b)` / `TA.cross(a, b)` | crossings ← a boolean array |
| `TA.rising(v, n)` / `TA.falling(v, n)` | rising/falling for `n` consecutive candles |
| `TA.barssince(cond)` | the number of candles since the last `true` |
| `TA.valuewhen(cond, src, occ)` | `src`'s value at the last occurrence (`occ` = the occurrence number) |
| `TA.pivothigh(v, left, right)` / `TA.pivotlow(...)` | pivot highs/lows — a value at the pivot, `NaN` elsewhere |
| `TA.nz(x, r)` / `TA.na(x)` | the replacement for `na`: substitute / test |

> **The recommended pattern with crossings:** lift both averages to full length,
> then cross them:
> ```javascript
> const f = TA.align(candles, TA.ema(close, 12), 11);
> const s = TA.align(candles, TA.ema(close, 26), 25);
> const buy = TA.crossover(f, s);          // a boolean array the length of the candles
> ```

### Time and sessions

`Intl` **does not exist** in the execution environment, so do not try to use
`toLocaleString` or `Intl.DateTimeFormat` — use these two instead. US daylight
saving is accounted for in both.

| Function | Description |
|---|---|
| `TA.tz(unixSeconds, zone)` | decomposes a time for a timezone |
| `TA.inSession(unixSeconds, 'HHMM-HHMM', zone)` | is the time inside a session? the full Pine syntax |

The session string accepts **Pine's syntax verbatim**:

| Form | Meaning |
|---|---|
| `'0930-1600'` | one range — the start is inclusive, the end is **exclusive** |
| `'2000-0400'` | crosses midnight |
| `'0000-0000'` | the whole day |
| `'1000-1100,1400-1500'` | two or more comma-separated ranges |
| `'0930-1600:23456'` | day suffix — **1 = Sunday** … 7 = Saturday (here Mon–Fri) |

A range crossing midnight belongs to **the day it starts on**, as in Pine:
`'2000-0400:23456'` includes Saturday's small hours (Friday's tail) and excludes
Monday's (Sunday's tail, which is disallowed). `npm run test:session` guards this.

`zone` is either an IANA identifier (`'Europe/London'`), a shorthand
(`'NY'`/`'LDN'`/`'TOK'`/`'SYD'`), an offset in hours (`3` = GMT+3), or left empty
so that **the indicator's timezone** is used — the one declared in
`meta.timezone` or in a `type:'timezone'` input (see "the session-time
translator" above). An indicator that has not declared its timezone keeps New
York as its default, for compatibility.

```javascript
const t = TA.tz(candle.time);          // { y, mo, d, h, mi, dow, key }
t.h                                     // the hour in the indicator's timezone
t.dow                                   // 0 = Sunday
t.key                                   // 20240724 — the day key, for grouping sessions

TA.inSession(candle.time, '0930-1100')  // the New York morning session
TA.inSession(candle.time, '2000-0000')  // the Asian session (crosses midnight)
```

> **Note:** on daylight-saving transition days some hours do not exist at all
> (02:00 jumps to 03:00 in March). A session starting at 02:00 will start at
> 03:00 that day — that is correct, not a bug.

`Math` and `JSON` are fully available. Anything else (`fetch`, `window`,
`setTimeout`, `Intl`, `Date` with local timezones) does not exist.

> **`console.log` works for debugging.** Its messages (and `console.warn` /
> `console.error`) appear in the test panel inside the editor after pressing
> "Test" — there alone, with no effect whatsoever on the chart, and they appear
> even if the code throws (the messages before the error). This is our answer to
> PineScript's lack of debugging tools: print any value during the calculation
> and inspect it. The limit is 200 messages.

---

## 5. The PineScript conversion table

| PineScript | Sovereign JS |
|---|---|
| `input.int(14, "Period")` | a `type:'int'` input, then `inputs.period` |
| `input.bool` / `input.color` / `input.session` | `type: 'bool'` / `'color'` / `'session'` |
| `input.string(options=[…])` | `type: 'select'` with `options` |
| `group=` / `inline=` in Pine | `section:` / `inline:` |
| `close` / `high` / `hlc3` | `TA.src(candles)` / `TA.src(candles,'high')` / `TA.hlc3(candles)` |
| `ta.sma(close, n)` | `TA.sma(TA.src(candles), n)` |
| `ta.rma(x, n)` | `TA.rma(x, n)` |
| `ta.atr(n)` | `TA.atr(candles, n)` |
| `ta.highest(high, n)` | `TA.highest(TA.src(candles,'high'), n)` |
| `ta.change(x)` | `TA.change(x)` |
| `ta.macd` / `ta.bb` / `ta.cci` / `ta.mfi` / `ta.wpr` | `TA.macd` / `TA.bb` / `TA.cci` / `TA.mfi` / `TA.wpr` |
| `ta.wma` / `ta.hma` / `ta.vwma` / `ta.vwap` | `TA.wma` / `TA.hma` / `TA.vwma` / `TA.vwap` |
| `ta.crossover(a, b)` | `TA.crossover(a, b)` (two arrays the length of the candles — `TA.align` first) |
| `ta.barssince` / `ta.valuewhen` / `ta.pivothigh` | `TA.barssince` / `TA.valuewhen` / `TA.pivothigh` |
| `x[1]` (a previous value) | `arr[i - 1]` |
| `var float s = na` + `s := …` | an ordinary variable in a `for` loop |
| `time("", "0930-1100", "America/New_York")` | `TA.inSession(candle.time, '0930-1100', 'America/New_York')` |
| `input.timezone(…)` | a `type:'timezone'` input (or `meta.timezone`) |
| `input.symbol(…)` | a `type:'symbol'` input — it opens the platform's symbol list, and empty = the chart's symbol |
| `hour` / `dayofweek` (in a timezone) | `TA.tz(candle.time).h` / `.dow` |
| `syminfo.timezone` | `'exchange'` as the timezone value |
| `plot(x)` | a `{ type: 'line', data: … }` plot |
| `plot(x, style=histogram)` | `{ type: 'histogram', data: … }` |
| `hline(70)` | `meta.levels: [70]` |
| `box.new(l, t, r, b)` | `{ kind: 'box', x1, y1, x2, y2 }` |
| `line.new(x1, y1, x2, y2)` | `{ kind: 'line', x1, y1, x2, y2 }` |
| `label.new(x, y, txt)` | `{ kind: 'label', x, y, text }` |
| `label.style_label_right` / `_left` / `_up` … | `style: 'label_right'` / `'label_left'` / `'label_up'` … |
| `font.family_default` / `font.family_monospace` | `font: 'sans'` / `'mono'` (and also `'serif'`) |
| `linefill.new(l1, l2, color)` | `{ kind: 'linefill', line1, line2, color }` |
| `xloc.bar_index` / `xloc.bar_time` | `xloc: 'bar_index'` on the shape / the default (time) |
| `request.security(sym, tf, expr)` | `request.security(sym, tf)` then calculate with `TA` and project with `request.map` (section 3.4) |
| `barmerge.lookahead_on` | `request.map(…, { lookahead: true })` |
| `polyline.new(points, curved, closed, fill_color)` | `{ kind: 'polyline', points, curved, closed, fillColor }` |
| `chart.point.from_index(i, price)` | `xloc: 'bar_index'` with `x: i` |
| `chart.point.from_time(t, price)` | the default — `x: t` |
| `.set_right()` / `.set_x2()` and the like | compute the final value directly — there is no incremental updating |
| `.delete()` / the `max_boxes_count` / `max_polylines_count` / `max_lines_count` limits | do not create the shape at all — trim the list before returning (the ceiling is 1,500 shapes) |
| `line.all` / `box.all` / `polyline.all` | no counterpart and none needed: shapes are built from scratch on every calculation |
| `barstate.isfirst` / `barstate.islast` | `i === 0` / `i === candles.length - 1` inside your loop |
| `barstate.isconfirmed` | `i < candles.length - 1` (only the last one is unconfirmed) |
| `var` / `varip` | an ordinary variable before the `for` loop |
| `plotshape(cond, style=shape.triangleup)` | `{ type: 'markers', markers: [...] }` (section 3.2) |
| `plot(x, style=area/stepline/baseline)` | `{ type: 'area' / 'stepline' / 'baseline' }` |
| `plotcandle(o,h,l,c)` / `plotbar(...)` | `{ type: 'candle' / 'bar', data: [{time,open,high,low,close}] }` |
| `fill(p1, p2, color)` | `{ type: 'band', data: [{time, upper, lower}] }` (works in the lower pane) — or `{ kind: 'fill' }` in shapes |
| `plot(x, style=plot.style_circles)` | `{ kind: 'circle' }` or `markers` with `shape:'circle'` |
| `table.new(pos)` + `table.cell(...)` | `{ kind: 'panel', position, rows }` |
| `table.cell(width=, height=)` | `width` / `height` on the cell (% of the chart) |
| `syminfo.ticker` / `syminfo.tickerid` | `syminfo.ticker` |
| `timeframe.period` | `timeframe.period` (the platform's notation) or `timeframe.pine` |
| `timeframe.isintraday` / `.isdaily` / `.multiplier` | the same names |
| `bgcolor(color)` | `{ type: 'bgcolor', data: [{time, color}] }` (section 3.5) |
| `input.source(close)` | `{ id, type: 'source', def: 'close' }` — it reaches `compute` as **an array** (section 3.6) |
| `input.source` from another indicator | supported: the user picks any active indicator's output from the same list |
| `alert()` / `alertcondition()` | **not supported** |

### Fundamental differences

1. **There is no per-candle execution.** PineScript runs once per candle and has
   automatic memory; here `compute` is called **once** with all the candles.
   Write an explicit `for` loop.

2. **State is manual.** The replacement for Pine's `var` is a variable outside
   the loop:

```javascript
// Pine:  var float trend = 1.0
//        trend := close > x ? 1 : -1
let trend = 1;
for (let i = 0; i < n; i++) {
  trend = closes[i] > x[i] ? 1 : -1;
}
```

3. **`na` becomes a gap** — do not return `NaN`, return `{ time }` with no `value`.

4. **Shapes are built once, final.** In Pine you create a box with `box.new` and
   then update it with `set_right()` on every candle. Here, pass over the candles
   first to determine the shape's start and end, then create it once with its
   final values.

```javascript
// Pine:  if newSession: box.new(...)  then  box.set_right(time) on every candle
// here: collect the session's range first
const runs = [];
let cur = null;
for (const c of candles) {
  if (TA.inSession(c.time, '0930-1100')) {
    if (!cur) { cur = { t1: c.time, t2: c.time, hi: c.high, lo: c.low }; runs.push(cur); }
    else { cur.t2 = c.time; cur.hi = Math.max(cur.hi, c.high); cur.lo = Math.min(cur.lo, c.low); }
  } else cur = null;
}
// then create the boxes in one go
const boxes = runs.slice(-maxDays).map(r =>
  ({ kind: 'box', x1: r.t1, y1: r.hi, x2: r.t2, y2: r.lo, bgColor: '...' }));
```

5. **The replacement for `max_boxes_count` and `.delete()`** — do not manage the
   shapes' lifecycle. Trim the list with `.slice(-n)` before returning, and stay
   within the 1,500-shape limit.

---

## 6. Limits

| Limit | Value | On exceeding |
|---|---|---|
| Execution time | 2 seconds | "The code timed out" |
| Memory | 64MB | "Memory limit exceeded" |
| Number of points | 200,000 | "Maximum number of points exceeded" |
| Number of shapes | 1,500 | the excess is ignored silently |
| Number of markers | 2,000 | the excess is ignored silently |
| Points in one fill | 60,000 | truncated |
| Number of panels (`panel`) | 4 per indicator | the excess is ignored |
| Panel rows/columns | 24 × 8 | truncated |
| Panel cell text | 64 characters | truncated |
| Number of plots | 8 | the excess is ignored |
| Code size | 120KB | saving is refused |

Panel cells count against the 1,500-shape ceiling individually, not as one shape
— a 10×3 panel consumes 30 of it.

The code runs inside a QuickJS sandbox: no access to the network, the page or
storage. Invalid values (colours, widths, texts) are sanitised automatically
rather than the indicator being rejected.

---

## 7. Complete example — Supertrend

```javascript
const meta = {
  name: 'Supertrend',
  label: 'Supertrend (ATR Trailing Stop)',
  color: '#26a69a',
  overlay: true,
  inputs: [
    { id: 'period', type: 'int', label: 'ATR period', def: 10, min: 1, max: 100, control: 'slider' },
    { id: 'mult', type: 'float', label: 'Multiplier', def: 3.0, min: 0.5, max: 10, step: 0.1 },
  ],
};

function compute(candles, inputs) {
  const n = inputs.period, mult = inputs.mult;
  const hl2 = TA.hl2(candles);
  const atr = TA.atr(candles, n);          // offset n-1 — matches ta.atr

  const up = [], dn = [];
  let prevUp = null, prevDn = null, trend = 1;

  for (let i = 0; i < candles.length; i++) {
    const t = candles[i].time;
    const k = i - (n - 1);                  // the ATR index matching candle i
    if (k < 0 || i === 0) { up.push({ time: t }); dn.push({ time: t }); continue; }  // warm-up (and i=0, since the line below reads the previous candle)

    let u = hl2[i] - mult * atr[k];
    let d = hl2[i] + mult * atr[k];

    // the trailing constraints — the replacement for  up := close[1] > up1 ? max(up, up1) : up
    const pc = candles[i - 1].close;
    if (prevUp !== null && pc > prevUp) u = Math.max(u, prevUp);
    if (prevDn !== null && pc < prevDn) d = Math.min(d, prevDn);

    if (prevUp !== null) {
      if (trend === -1 && candles[i].close > prevDn) trend = 1;
      else if (trend === 1 && candles[i].close < prevUp) trend = -1;
    }

    // a gap on the inactive side ← the line breaks when the trend reverses
    if (trend === 1) { up.push({ time: t, value: u }); dn.push({ time: t }); }
    else             { dn.push({ time: t, value: d }); up.push({ time: t }); }

    prevUp = u; prevDn = d;
  }

  return [
    { id: 'up', type: 'line', primary: true, data: up },
    { id: 'dn', type: 'line', style: { color: '#ef5350', lineWidth: 2, title: '' }, data: dn },
  ];
}
```

> **A recommended pattern:** note that the loop runs over **the candle index**
> rather than the computed array's index, and converts with `k = i - offset`.
> This is less prone to offset errors than looping over the short array, and it
> makes binding each point to its candle obvious.

---

## 7.1 A shapes example — session boxes

It demonstrates the full pattern: detecting the session by time, grouping the
candles, trimming the list, then building the boxes, lines and labels in one go.

```javascript
const meta = {
  name: 'Sessions', label: 'Killzone Sessions', color: '#2196F3', overlay: true,
  inputs: [
    // the SESSIONS times below are interpreted in it — without this input the default is New York
    { id: 'zone', type: 'timezone', label: 'Timezone', def: 'chart' },
    { id: 'maxDays', type: 'int', label: 'Session limit', def: 3, min: 1, max: 20, control: 'slider' },
  ],
};

const SESSIONS = [
  { session: '2000-0000', text: 'Asia',  hi: 'AS.H',   lo: 'AS.L',   rgb: [41, 98, 255] },
  { session: '0200-0500', text: 'London', hi: 'LO.H',  lo: 'LO.L',   rgb: [255, 82, 82] },
  { session: '0930-1100', text: 'NY AM',  hi: 'NYAM.H', lo: 'NYAM.L', rgb: [8, 153, 129] },
];

function compute(candles, inputs) {
  if (!candles.length) return [{ id: 'kz', type: 'shapes', shapes: [] }];
  const lastTime = candles[candles.length - 1].time;
  const shapes = [];

  for (const S of SESSIONS) {
    const rgb = S.rgb.join(',');
    const boxColor = `rgba(${rgb},0.3)`, lineColor = `rgba(${rgb},1)`;

    // 1) group the consecutive candles inside the session
    const runs = [];
    let cur = null;
    for (const c of candles) {
      if (TA.inSession(c.time, S.session)) {
        if (!cur) { cur = { t1: c.time, t2: c.time, hi: c.high, lo: c.low }; runs.push(cur); }
        else { cur.t2 = c.time; cur.hi = Math.max(cur.hi, c.high); cur.lo = Math.min(cur.lo, c.low); }
      } else cur = null;
    }

    // 2) only the last maxDays sessions — the replacement for max_boxes_count and .delete()
    for (const K of runs.slice(-inputs.maxDays)) {
      shapes.push({ kind: 'box', x1: K.t1, y1: K.hi, x2: K.t2, y2: K.lo,
                    bgColor: boxColor, borderColor: boxColor,
                    text: S.text, textColor: lineColor });

      // 3) the high/low lines run until the break
      let hiEnd = lastTime, loEnd = lastTime, hiHit = false, loHit = false;
      for (const c of candles) {
        if (c.time <= K.t2) continue;
        if (!hiHit && c.high > K.hi) { hiEnd = c.time; hiHit = true; }
        if (!loHit && c.low < K.lo)  { loEnd = c.time; loHit = true; }
        if (hiHit && loHit) break;
      }

      shapes.push({ kind: 'line', x1: K.t1, y1: K.hi, x2: hiEnd, y2: K.hi, color: lineColor });
      shapes.push({ kind: 'line', x1: K.t1, y1: K.lo, x2: loEnd, y2: K.lo, color: lineColor });
      shapes.push({ kind: 'label', x: K.t1, y: K.hi, text: S.hi, textColor: lineColor, position: 'above', size: 10 });
      shapes.push({ kind: 'label', x: K.t1, y: K.lo, text: S.lo, textColor: lineColor, position: 'below', size: 10 });
    }
  }
  return [{ id: 'kz', type: 'shapes', shapes: shapes }];
}
```

---

## 8. The ready-made prompt — in two stages

Serious indicators exceed a hundred inputs. When the model is asked to produce
the settings and the algorithm in a single reply, the two compete for its
attention and for the output's length — so it favours the algorithm and cuts the
settings short. **Separating them ends that competition**, and lets you review
the settings list before the calculation is written at all.

### Stage 1 — extracting the settings

> Upload this file + the PineScript code, and copy what is between the two rules.

---

```
Read the attached PineScript indicator and extract only its settings. Do not write any
calculation code yet.

Two things are required:

First — an honesty table listing every `input` in the source:

| The Pine input | Type | Our counterpart | Note |

The rule: show every input that **changes what is actually drawn** — even one disabled
by default (input.bool(false)); show it as a bool with def: false.
Do not show inputs tied to unsupported features (in practice only alerts); instead write
the reason it is impossible in the note column. As for `bgcolor` and `input.source`
inputs, do show them: both are supported (sections 3.5 and 3.6), and `input.source`
becomes `type:'source'`. As for table inputs (`table`) — its position, colours and font
size — show them all: the table is supported via `kind:'panel'`. Likewise the
symbol/timeframe display inputs: both are available. And a higher-timeframe input
(`input.timeframe` or a timeframe string) — show it as a `select` or `string` and pass it
to `request.security`; fetching another timeframe/symbol is now supported (section 3.4).

Second — end with an explicit numeric line:
"Converted N of M inputs. Not converted: X for unsupported features, Y for another reason
(state it)."

Then output the complete `const meta = {...}` alone — with no `compute`.

Use the nine types from section 2 (int / float / bool / color / string /
select / session / time / symbol — the last for every input that picks a symbol), and
organise the inputs with `section` for the sections and `inline` to gather what belongs to
one element on a single row (the toggle, the name, the time and the colour together).
The ceiling is 80 inputs; if the source exceeds it, drop the least consequential ones and
say what you dropped.
```

---

### Stage 2 — writing the calculation

> After reviewing `meta` and adjusting what you want, send this in the same conversation.

---

```
Approved. Now write `function compute(candles, inputs)` for this meta.

Follow the attached reference (CUSTOM-INDICATORS.en.md) closely, and abide by the
following:

0. Use every input we declared in meta. Any input that does not affect the output is a
   dead option in the UI; if one of them cannot be implemented, say so explicitly rather
   than leaving it dangling.
1. Output a single file containing only `const meta = {...}` and
   `function compute(candles, inputs)`.
2. Use only the TA functions available in the reference. Do not invent functions that do
   not exist. If you need a function that is missing (such as ATR), derive it from what is
   there.
3. Mind the arrays' lengths and the offset — this is the biggest source of errors.
   Verify that every point is bound to the correct candle via TA.pt or an explicit index.
4. Convert Pine's `var` and `:=` logic into variables outside an explicit for loop,
   because compute is called once with all the candles rather than once per candle.
5. Instead of NaN/na use a point with no value: { time } — it is drawn as a gap.
6. Set overlay: true if the indicator draws on the price, and false if it has an
   independent scale.
7. Convert hline into meta.levels, and do not set explicit colours for levels.
8. For boxes, lines, labels, circles, fills and panels use "shapes"
   (section 3.1), and for plotshape signals use "markers" (section 3.2).
   Coordinates are time and price, not the candle's index. Do not emulate box.new with
   incremental updating: compute the shape's start and end first, then create it once.
   Every type — shapes, fills, markers and the bgcolor background — works in the lower
   pane too (overlay: false). For bgcolor() use `{ type:'bgcolor' }` (section 3.5),
   and for input.source use `{ type:'source' }` (section 3.6).
9. For sessions and times use TA.inSession and TA.tz — do not use Intl or
   toLocaleString, they do not exist. And add a `type:'timezone'` input to any session
   indicator, otherwise its times are interpreted in New York time whatever the user's
   axis is.
10. Honour a parameter that caps the number of shapes (such as maxDays) and trim the list
    with slice(-n). The maximum is 1,500 shapes, and panel cells count against it.
10.1 Convert table.new/table.cell into `{ kind:'panel', position, rows }` inside
    "shapes" (section 3.1) — a pixel-pinned panel that does not drift with scrolling, and
    width/height on the cell are a percentage of the chart as in Pine. Do not substitute a
    label for it: a label is anchored to a time and a price, so it drifts out of place and
    off the screen.
10.2 `syminfo.ticker` and `timeframe.period` are available inside compute (section 3.3),
    and `timeframe.pine` gives Pine's own notation ('15'/'240'/'D'). Do not delete the
    lines that use them and do not replace them with fixed text.
11. Do not use fetch or window or setTimeout — they do not exist in the sandbox.
    (`console.log` works for debugging: it appears in the editor's test panel only, with
    no effect on the chart.)
11.1 `request.security` is supported (section 3.4): convert it into
    `request.security(sym, tf)` + a calculation with `TA` + `request.map`, and make the
    code tolerate an empty array on the first run. And `barmerge.lookahead_on`
    corresponds to `{ lookahead: true }` — without it the indicator does not repaint,
    which is the correct default.
12. If the source has unsupported features (in practice, alerts), state them explicitly in
    a comment at the top of the file rather than ignoring them silently, and convert the
    rest. And do not declare anything unsupported before finding it as such in the
    conversion table (section 5) — tables, the symbol and the timeframe, fetching another
    timeframe/symbol, the bgcolor background, the price source and markers in the lower
    pane are all supported.
13. Options disabled by default in the source should actually be implemented in the
    calculation — their default value of false is enough to hide them, there is no need to
    delete the code.
14. **If the source draws in a separate pane as rows** (ICT Quarterly, session maps,
    gauges under the chart): set `overlay: false`, and use fixed row coordinates
    (`y = 0,1,2…`) **not prices** (high/low). Review the "drawing in a separate sub-pane"
    section. If the source combines drawing on the price and drawing in a pane at once (a
    display-on-chart switch per cycle), that is impossible in a single indicator: pick the
    row-based pane and drop the dual switch, and mention it in a comment at the top of the
    file.

Output the code only, ready to paste into the editor.
```

---

### If you want the conversion in a single reply

For small indicators (fewer than 10 inputs) the split is not necessary — send
both prompts together, preceded by "do both stages in one reply". For large
indicators **do not**; that is exactly what produces a poor settings panel.

---

**If the code does not work on the first try:** copy the error message from the
editor's panel and hand it back to the AI — most problems are solved in an
attempt or two, and the most common of them is an offset error.

**If the settings come out poor anyway:** ask for the honesty table explicitly —
"how many inputs are in the source and how many did you convert?". The number
exposes silent omission immediately.
