---
title: "Stats, history and lists"
description: "The ./sdk/stats module is the window arithmetic under the TA library and the extra indicators. It holds a stats namespace of allocation-free functions over a…"
order: 48
section: "functions"
---

<!-- source: docs/indicators/functions/stats-history-lists.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Stats, history and lists

The `./sdk/stats` module is the window arithmetic under the
[TA library](ta-library.md) and the [extra indicators](extra-indicators.md).
It holds a `stats` namespace of allocation-free functions over a
`StaticArray<f64>` you fill yourself, a `History` that keeps the last `n`
values of a series so Pine's `close[1]` becomes `history.ago(1)`, a
fixed-capacity `List` for Pine's `array.*` idiom, a `HandleRing` that
keeps the newest N drawing ids so the oldest line can be deleted,
`roundTo` for `math.round(x, n)`, `roundToTick` for
`math.round_to_mintick(x)` and a seeded `Random` for
`math.random(min, max, seed)`. Everything here composes the library's own
primitives, so a window mean is the library's `Sma` to the last bit, and
the library's rules hold: `NaN` until a class is warm, `NaN` while a
non-finite entry sits in its window, and no allocation after the
constructor.

## Which one

| Need | Reach for |
| --- | --- |
| The previous bar's value of a series you compute, Pine's `x[n]` | `History`: `push()` once per bar, read `ago(n)` |
| Entries that come and go on their own schedule, by index, Pine's `array.*` | `List`, with its capacity declared in `onStart()` |
| Several readings over one window you fill yourself | the `stats.*` functions over a `StaticArray<f64>` and a count |
| The ids of the newest N drawings, so the oldest can be deleted | `HandleRing` |
| `math.round(x, n)`, `math.round_to_mintick(x)`, `math.random(min, max, seed)` | `roundTo`, `roundToTick`, `Random` |

## Window arithmetic: `stats`

`stats` is a namespace of functions over the first `n` entries of a
`StaticArray<f64>` you own. Nothing here allocates: you size the array
in `onStart()`, fill it as bars arrive, and hand the count you filled. A
count above the array's length reads the whole array; a count below 1
reads nothing. A non-finite entry among the first `n` makes the result
`NaN`, except in `min`, `max`, `argmin` and `argmax`, which skip it.

| Function | Returns |
| --- | --- |
| `stats.sum(a, n)` | the entries added index 0 first; 0 when `n < 1` |
| `stats.mean(a, n)` | `sum / n`; `NaN` when `n < 1` |
| `stats.variance(a, n)` | the population variance (divide by `n`, the library's `Variance`) |
| `stats.stdev(a, n)` | `sqrt(variance)` |
| `stats.min(a, n)` | the smallest finite entry; `NaN` when none is finite |
| `stats.max(a, n)` | the largest finite entry; `NaN` when none is finite |
| `stats.argmin(a, n)` | the index of the smallest finite entry (the lowest index on a tie); `-1` when none |
| `stats.argmax(a, n)` | the index of the largest finite entry (the lowest index on a tie); `-1` when none |
| `stats.slope(a, n)` | the least-squares slope against the index `0..n-1`, in value per entry; `NaN` when `n < 2` |
| `stats.covariance(a, b, n)` | the population covariance of the two arrays' first `n` entries |
| `stats.correlation(a, b, n)` | the Pearson correlation, `-1..1`; `NaN` when either side has no variance |
| `stats.zscore(x, a, n)` | `(x - mean) / stdev`; 0 when the standard deviation is 0 (the library's `Zscore`) |
| `stats.sortAscending(a, n)` | sorts the first `n` entries in place, ascending, `NaN` entries last |
| `stats.median(a, n, scratch)` | the middle of the sorted entries (the mean of the two middle ones on an even `n`); copies into `scratch` and sorts there, so `a` keeps its order |
| `stats.percentile(a, n, pct, scratch)` | the nearest-rank percentile, `pct` in `0..100`: rank `ceil(pct / 100 * n)`, entry `rank - 1` of the sorted copy (the library's `Percentile`) |
| `stats.percentileLinearInterpolation(a, n, pct, scratch)` | the percentile by linear interpolation between the two nearest ranks, `pct` in `0..100`: TradingView's position `k = pct * n / 100 - 0.5` over the sorted copy, `sorted[floor(k)]` plus the fraction of the way to `sorted[floor(k) + 1]`, the smallest entry at or below `k = 0` and the largest at or above `k = n - 1` (so `array.from(3, 1, 8, 5)` reads `1, 1, 2, 3.2, 4, 5, 6.5, 8, 8` at `0, 10, 25, 40, 50, 62.5, 75, 90, 100`, the values captured on TradingView); the result need not be an entry (Pine's `array.percentile_linear_interpolation` and `ta.percentile_linear_interpolation`) |

`scratch` is a second array of at least `n` you allocate once beside the
first; `median` and the two percentiles copy into it and sort it, and return
`NaN` when it is too small. The fence keeps a sliding window of the last
`period` closes and volumes, oldest first, and reads it every bar.

```typescript sample=fn-extra-indicators-stats
param("period", 50, { min: 2, max: 400, description: "Bars in the window" });
output("mean", line, lower, { color: "#2563eb", description: "Mean close" });
output("stdev", line, lower, { color: "#7c3aed", description: "Standard deviation" });
output("slope", line, lower, { color: "#16a34a", description: "Least-squares slope, price per bar" });
output("correlation", line, lower, { color: "#ea580c", description: "Close-volume correlation" });
output("zscore", line, lower, { color: "#dc2626", description: "Z-score of this close" });
output("median", line, lower, { color: "#0891b2", description: "Median" });
output("p90", line, lower, { color: "#4b5563", description: "90th percentile of the close" });

let period: i32 = 50;
let closes = new StaticArray<f64>(50);
let volumes = new StaticArray<f64>(50);
let scratch = new StaticArray<f64>(50);
let filled: i32 = 0;

function onStart(): void {
  period = i32(p_period());
  closes = new StaticArray<f64>(period);
  volumes = new StaticArray<f64>(period);
  scratch = new StaticArray<f64>(period);
}

function onBar(): void {
  const close = bar.close();
  const volume = bar.volume();
  // Slide the window one bar: drop the oldest, append the newest.
  if (filled < period) filled += 1;
  else {
    for (let k = 1; k < period; k++) {
      unchecked((closes[k - 1] = closes[k]));
      unchecked((volumes[k - 1] = volumes[k]));
    }
  }
  unchecked((closes[filled - 1] = close));
  unchecked((volumes[filled - 1] = volume));
  if (filled < period) return;
  out_mean(stats.mean(closes, filled));
  out_stdev(stats.stdev(closes, filled));
  out_slope(stats.slope(closes, filled));
  out_correlation(stats.correlation(closes, volumes, filled));
  out_zscore(stats.zscore(close, closes, filled));
  out_median(stats.median(closes, filled, scratch));
  out_p90(stats.percentile(closes, filled, 90.0, scratch));
}
```

The library's `Sma`, `Stdev`, `Correlation`, `Median` and `Percentile`
give the same numbers with their own rings; reach for `stats` when one
window feeds several readings or is not a fixed number of bars (a
session, a swing, the bars since a signal).

## History: `x[n]` from Pine

`History` keeps the last `size` values of one series. Construct it in
`onStart()`, `push()` once per bar, and read `ago(n)`: `ago(0)` is the
value just pushed, `ago(1)` the previous bar's.

| Member | Meaning |
| --- | --- |
| `new History(size)` | keep the last `size` values (a size below 1 keeps one) |
| `push(v)` | record this bar's value; the oldest one falls out once full |
| `ago(n)` | the value pushed `n` bars ago; `NaN` before `n + 1` pushes or outside `0..size - 1` |
| `latest()` | `ago(0)` |
| `count()` | how many values it holds, at most `size` |
| `size()` | the size it was constructed with |
| `max()`, `min()`, `mean()`, `sum()` | over the values held; `NaN` while empty or when one of them is not finite |
| `reset()` | empty it |

Two Pine idioms port directly: `close[1]` is
`history.ago(1)`, and `ta.highest(close, 20)` is `max()` over a
`History(20)` of the close (the library's `Highest(20)` gives the same
number and the offset of the high as well).

```typescript sample=fn-extra-indicators-history
output("prev_close", line, overlay, { color: "#4b5563", description: "close[1]: the previous bar's close" });
output("highest", line, overlay, { color: "#16a34a", description: "Highest close of the last 20 bars" });
output("change", line, lower, { color: "#2563eb", description: "close - close[1]" });

let history = new History(20);
let highest = new Highest(20);

function onStart(): void {
  history = new History(20);
  highest = new Highest(20);
}

function onBar(): void {
  const close = bar.close();
  history.push(close);
  const prevClose = history.ago(1);
  const highestClose = history.max();
  // The library class over the same 20 bars reads the same high.
  const check = highest.update(close);
  if (!isNaN(check) && check != highestClose) return;
  if (isNaN(prevClose)) return;
  out_prev_close(prevClose);
  out_highest(highestClose);
  out_change(close - prevClose);
}
```

`history.max()` reads the values held so far: on bar 5 it is the high of
six bars while `Highest(20)` is still `NaN`, and from bar 19 on the two
agree (the fence checks it). Reach for `History` to look back on a
series you compute yourself, an RSI or a spread.

## A percent rank of volume

Where does this bar's volume sit against the last 100 bars? `PercentRank`
([Extra indicators](extra-indicators.md#deviation-rank-and-width))
answers it directly, and a `History` of the volumes hands the same window
to `stats.percentile` for the threshold the rank crossed. The mark draws
on bars whose volume ranks in the top decile.

```typescript sample=fn-extra-indicators-volume-rank
param("period", 100, { min: 2, max: 400, description: "Bars in the window" });
input("volume", ohlcv.volume);
output("rank", line, lower, { color: "#2563eb", description: "Percent rank of the volume" });
output("p90", line, lower, { color: "#4b5563", description: "90th percentile of volume" });
output("heavy", none, overlay, { description: "1 when the rank is 90 or above" });
output("heavy_mark", shape, overlay, { color: "#ea580c", shape_where: "heavy", description: "The close on a heavy-volume bar" });

let rank = new PercentRank(100);
let history = new History(100);
let window = new StaticArray<f64>(100);
let scratch = new StaticArray<f64>(100);

function onStart(): void {
  const period = i32(p_period());
  rank = new PercentRank(period);
  history = new History(period);
  window = new StaticArray<f64>(period);
  scratch = new StaticArray<f64>(period);
}

function onBar(): void {
  const close = bar.close();
  const volume = bar.volume();
  const rankValue = rank.update(volume);
  history.push(volume);
  // Copy the held volumes into the window, oldest first, and read it.
  const held = history.count();
  for (let k = 0; k < held; k++) unchecked((window[k] = history.ago(held - 1 - k)));
  const p90Value = stats.percentile(window, held, 90.0, scratch);
  const heavy = !isNaN(rankValue) && rankValue >= 90.0 ? 1.0 : 0.0;
  if (isNaN(rankValue)) return;
  out_rank(rankValue);
  out_p90(p90Value);
  out_heavy(heavy);
  out_heavy_mark(close);
}
```

## Lists: `List`

`List` is a fixed-capacity list of `f64` values, Pine's `array.*` idiom
with the one difference that the module declares its size up front.
Construct it in `onStart()` with the most entries it will hold (a capacity
below 1 keeps one; the buffer is allocated once), then push, insert,
remove, read and sort it on any bar: no method allocates after the
constructor, so a list never grows the module's memory per bar. Index 0
is the oldest entry and a negative index counts from the end, so
`get(-1)` is the last entry. Three conventions cover every miss: a read
outside the list is `NaN`, a change that does not fit (the list is full,
or the index is out of range) returns `false` and changes nothing, and
`pushEvict()` is the one way a full list takes a new value: it drops the
oldest entry and hands it back.

| Member | Meaning |
| --- | --- |
| `new List(capacity)` | hold at most `capacity` entries (below 1 keeps one); construct in `onStart()` |
| `capacity()`, `size()` | the limit, and how many entries are held now |
| `isEmpty()`, `isFull()` | `size() == 0`, `size() == capacity()` |
| `clear()` | empty the list; the capacity is unchanged |
| `push(v)` | append `v` as the last entry; `false` and no change when full |
| `pushEvict(v)` | append `v`; when full, first remove and return the oldest entry (index 0), else return `NaN` |
| `unshift(v)` | insert `v` at index 0; `false` when full |
| `pop()`, `shift()` | remove and return the last, or the first, entry; `NaN` when empty |
| `get(i)` | the entry at `i`; a negative `i` counts from the end (`-1` is the last); `NaN` outside the list |
| `set(i, v)` | overwrite the entry at `i`, the same indexing; `false` outside the list |
| `insert(i, v)` | insert `v` before index `i`, `i` in `0..size()` (`size()` appends); `false` when full or out of range |
| `remove(i)` | remove and return the entry at `i`, the same indexing as `get`; `NaN` outside the list |
| `first()`, `last()` | the entries at `0` and `size() - 1`; `NaN` when empty |
| `indexOf(v)`, `includes(v)` | the lowest index whose entry equals `v`, or `-1`; `NaN` never matches |
| `sort(ascending = true)` | sort in place; `NaN` entries sit last in either direction |
| `sum()`, `mean()`, `min()`, `max()`, `stdev()` | the `stats.*` definitions over the entries held (population `stdev`); `NaN` while empty or when an entry is not finite, the `History` rule |
| `values()` | the backing `StaticArray<f64>`: entries `0..size() - 1` are the list, so `stats.median(list.values(), list.size(), scratch)` reads it |

The members below give the rest of Pine's `array.*` calls their own
name. A Pine call that makes a second array (`array.copy`, `slice`,
`abs`, `standardize`, `sort_indices`) writes here into a destination you
constructed in `onStart()`: a second `List` with enough capacity (it may be
the list itself where that makes sense), or a `StaticArray<i32>` of at
least `size()` for the indexes. `median` and the two percentiles find
their entry by counting, so they need no scratch array and leave the
list's order alone, and they read a `NaN` (or infinite) entry as Pine's
`na`: `median` skips it, the percentiles count it in `size()` and sort
it last, as TradingView does. Nothing below allocates; `List.from` is
the one exception and belongs in `onStart()`.

| Member | Meaning |
| --- | --- |
| `List.from([v0, v1, ...], capacity = 0)` | a list holding the values in order, with room for `capacity` entries or for exactly the values given when `capacity` is below their count (`List.from([1.0, 2.0])` is full: size the capacity when the list will grow); construct in `onStart()` |
| `copy(into)` | replace the entries of `into` with this list's; `false` and no change when `into` cannot hold `size()` entries |
| `slice(into, from, to = size())` | write the entries from index `from` up to but not including `to` into `into`; a negative bound counts from the end, both are clamped to the list, a range that ends at or before it starts leaves `into` empty; `false` when `into` cannot hold the range; a copy, not Pine's live view |
| `fill(v, from = 0, to = size())` | set every entry in the same range to `v`, in place; the size is unchanged |
| `reverse()` | reverse the entries in place |
| `abs(into)` | write the absolute value of every entry into `into` (`into` may be the list itself); `false` when it cannot hold them |
| `standardize(into)` | write every entry's `(x - mean()) / stdev()` into `into` (`into` may be the list itself); every result `NaN` when the list is empty, holds a non-finite entry or has no spread; `false` when `into` cannot hold them |
| `every()`, `some()` | read the list as a Pine `array<bool>` held as `1.0` and `0.0`: an entry is true when it is neither `0` nor `NaN`; both are `false` for an empty list, as on TradingView (`array.every(array.new_bool(0))` is `false` there) |
| `sortIndices(into, ascending = true)` | write into the `StaticArray<i32>` the indexes `0..size() - 1` ordered by their entries, so `get(into[0])` is the smallest (or, with `false`, the largest) entry; ascending is stable (equal entries keep their index order) with the indexes of `NaN` entries last, and descending is that order reversed as on TradingView, so ties come in reverse index order and the `NaN` indexes first (`[2, 1, 2, 1, 2]` reads `1, 3, 0, 2, 4` and `4, 2, 0, 3, 1`; `[3, NaN, 1, 2]` reads `2, 3, 0, 1` and `1, 0, 3, 2`); the list is untouched; `false` when `into` is shorter than `size()` |
| `median()` | the middle entry of the finite entries sorted, the mean of the two middle ones when their count is even; a `NaN` entry is skipped as TradingView skips `na` (`[3, NaN, 1, 8, 5]` reads `4`); `NaN` while no entry is finite |
| `percentileNearestRank(pct)` | the nearest-rank percentile, `pct` in `0..100`: rank `ceil(pct / 100 * size())`, the entry at `max(0, rank - 1)` of the sorted list with the `NaN` entries last, so a rank that lands on one reads `NaN`; `NaN` while empty or when `pct` is not finite |
| `percentileLinearInterpolation(pct)` | the percentile by linear interpolation between the two nearest ranks: TradingView's position `k = pct * size() / 100 - 0.5` over the sorted list (`NaN` entries last), the entry at `floor(k)` plus the fraction of the way to the next one, the smallest entry at or below `k = 0` and the largest at or above `k = size() - 1`, so `50` is the median of an even count and the result need not be an entry; with a `NaN` entry held a whole position reads the entry there and a fractional one reads `NaN`, as on TradingView (`[3, NaN, 1, 8, 5]` reads `1` at `10`, `NaN` at `25`, `5` at `50`); `NaN` while empty or when `pct` is not finite |

The list below keeps impulse closes as levels: a close more than
`step_pct` above the previous one goes in through `pushEvict()`, so the
newest `keep` of them are held, and a level the close has risen above is
removed. Walking `remove()` from the end keeps the lower indexes valid.
The nearest level above price is the list's `min()`, and the mean level
is rounded with `roundTo` for the legend.

```typescript sample=fn-extra-indicators-list
param("keep", 8, { min: 1, max: 50, description: "Impulse closes kept" });
param("step_pct", 1, { min: 0.1, max: 20, description: "Rise from the previous close that marks an impulse, in percent" });
output("nearest_above", line, overlay, { color: "#16a34a", description: "The lowest kept level above the close" });
output("mean_level", line, overlay, { color: "#94a3b8", description: "Mean of the kept levels, two decimals" });
output("count", none, lower, { description: "Levels kept on this bar" });

let levels = new List(8);
let step: f64 = 0.01;
let prevClose: f64 = NaN;

function onStart(): void {
  // Construct in onStart(): the list's buffer is allocated once, sized by the param.
  levels = new List(i32(p_keep()));
  step = p_step_pct() / 100.0;
}

function onBar(): void {
  const close = bar.close();
  // An impulse close joins the list; once the list is full the oldest level leaves.
  if (!isNaN(prevClose) && close > prevClose * (1.0 + step)) levels.pushEvict(close);
  prevClose = close;
  // A level the close has risen above is spent: remove it, walking from the end.
  for (let i = levels.size() - 1; i >= 0; i--) {
    if (close > levels.get(i)) levels.remove(i);
  }
  // No level yet: the outputs stay unwritten and draw nothing on this bar.
  if (levels.isEmpty()) return;
  out_nearest_above(levels.min());
  out_mean_level(roundTo(levels.mean(), 2));
  out_count(f64(levels.size()));
}

function onReset(): void {
  levels.clear();
  prevClose = NaN;
}
```

Only a `List` hands its entries to the `stats` functions, through
`values()` and `size()`; a `History` is read with `ago(n)` and its own
aggregates, or copied into a window as the volume-rank sample above does.

## Percentiles of a list

The close against its own recent spread: the 10th and 90th percentiles of
the last closes, their median, and how long ago the highest of them closed,
each level rounded to the market's tick.

```typescript sample=fn-stats-list-percentiles
// The close against its own recent spread: the 10th and 90th percentiles of the last closes and their median, on the market's tick.
param.int("keep", 50, { min: 5, max: 500, label: "Closes kept" });
market.tick_size();
market.price_precision();
output("p90", line, overlay, { color: "#16a34a", description: "90th percentile of the kept closes" });
output("median", line, overlay, { color: "#94a3b8", line_style: "dashed", description: "Median of the kept closes" });
output("p10", line, overlay, { color: "#dc2626", description: "10th percentile of the kept closes" });
output("top_age", none, lower, { description: "Bars since the highest kept close" });

let closes = new List(50);
let order = new StaticArray<i32>(50);
let tick: f64 = 0;
let decimals: i32 = 2;

function onStart(): void {
  const keep = i32(p_keep());
  closes = new List(keep);
  order = new StaticArray<i32>(keep);
  tick = p_market_tick_size();
  decimals = i32(p_market_price_precision());
}

// A level on the market's tick where it publishes one, else at its price decimals.
function snap(x: f64): f64 {
  return tick > 0 ? roundToTick(x, tick) : roundTo(x, decimals);
}

function onBar(): void {
  const close = bar.close();
  if (isNaN(close)) return;
  // Keep the newest closes: once the list is full, the oldest leaves.
  closes.pushEvict(close);
  if (!closes.isFull()) return;
  out_p90(snap(closes.percentileLinearInterpolation(90)));
  out_median(snap(closes.median()));
  out_p10(snap(closes.percentileLinearInterpolation(10)));
  // The indexes ordered from the highest close down: order[0] is where the highest sits, 0 the oldest.
  closes.sortIndices(order, false);
  out_top_age(f64(closes.size() - 1 - order[0]));
}
```

- **The newest closes.** `pushEvict()` appends the close and, once the list
  is full, drops the oldest, so the list always holds the last `keep`
  closes.
- **Percentiles and the median.** `percentileLinearInterpolation(90)` is
  Pine's `array.percentile_linear_interpolation(a, 90)`, and `median()` is
  `array.median(a)`. Both count their way to the answer, so the list keeps
  its order.
- **An order without sorting.** `sortIndices(order, false)` writes the
  indexes from the highest close down into `order`, a `StaticArray<i32>`
  made in `onStart()`. Ties come newest first, as on TradingView.
- **On the tick.** `roundToTick(x, tick)` puts each level on the market's
  tick. Where the tick reads 0 (only CME markets publish one today),
  `roundTo(x, decimals)` rounds to the chart's price decimals instead.

## Keep the last N drawings: `HandleRing`

Lines, boxes, labels and polylines share one id space, and a drawing stays
on the chart until its id is deleted
([Drawing objects](../presentation/drawing-objects.md)). "Keep only the newest five
lines" therefore means remembering the ids you created and deleting the
oldest when the sixth arrives. `HandleRing` is that memory: `push(id)`
records an id and, when the ring is already full, hands back the oldest
id for you to delete:

`const old = ring.push(id); if (old >= 0) lines[old].delete();`

| Member | Meaning |
| --- | --- |
| `new HandleRing(capacity)` | keep the newest `capacity` ids (below 1 keeps one); construct in `onStart()` |
| `push(id)` | record `id` as the newest; when full, first drop and return the oldest id (delete that drawing), else return `-1`; a negative id is not recorded and returns `-1` |
| `size()`, `capacity()` | how many ids are held, and the limit |
| `get(i)` | the id at `i`, `0` the oldest and `size() - 1` the newest; `-1` outside the ring |
| `oldest()`, `newest()` | `get(0)` and `get(size() - 1)`; `-1` when empty |
| `remove(id)` | forget an id you deleted early, so it is not handed back later; `false` when the ring does not hold it |
| `clear()` | forget every id; the drawings themselves stay |

Two rules from the drawing page shape the idiom. A handle object
allocates when it is made, so make them once in `onStart()` rather than
calling `draw.line(old)` on every eviction: let the ids cycle through one
more value than the ring keeps (`KEEP + 1`), make that many handle
objects, and the id you set next is never one still on the chart. And the
chart caps live handles at 500 per kind and 1500 in all, so a ring's
capacity sits well under those. The sample draws a dashed ray at every
new `period`-bar high and keeps the newest five:

```typescript sample=fn-extra-indicators-handle-ring
param("period", 20, { min: 2, max: 400, description: "Bars a high must top" });
output("highest", line, overlay, { color: "#f5a623", description: "Highest high of the window" });
output("lines", none, lower, { description: "Rays on the chart" });
handles.line({ color: "#f5a623", width: 1, lineStyle: "dashed", extend: "right" });

// Five rays stay on the chart. Ids cycle through six, one more than the ring keeps,
// so the id set next is never one still drawn; the six handle objects are made once.
const KEEP = 5;
const IDS = KEEP + 1;
const lines = new Array<LineHandle>();
let ring = new HandleRing(KEEP);
let highest = new Highest(20);
let nextId: i32 = 0;
let prevT: f64 = NaN;

function onStart(): void {
  highest = new Highest(i32(p_period()));
  ring = new HandleRing(KEEP);
  for (let id = 0; id < IDS; id++) lines.push(draw.line(id));
}

function onBar(): void {
  const t = bar.time();
  const high = bar.high();
  const top = highest.update(high);
  const width = isNaN(prevT) ? 60.0 : t - prevT;
  prevT = t;
  if (isNaN(top)) return;
  // bars == 0: this bar set the window's high.
  if (highest.bars == 0.0) {
    const id = nextId;
    nextId = (nextId + 1) % IDS;
    // Record the id; when the ring is full the oldest comes back and its ray goes.
    const old = ring.push(id);
    if (old >= 0) lines[old].delete();
    lines[id].set(t, high, t + width, high);
  }
  out_highest(top);
  out_lines(f64(ring.size()));
}

function onReset(): void {
  highest.reset();
  ring.clear();
  nextId = 0;
  prevT = NaN;
}
```

`lines` reads 1, 2, 3, 4 on the first four highs and 5 from then on: the
sixth high evicts the first ray's id, `delete()` removes it, and that id
is the one set on the very next high. A ray you delete for another reason (a
level broken, say) is also handed to `ring.remove(id)`, so the ring never
returns an id that is already free.

## Rounding: `roundTo`

`roundTo(x, decimals)` rounds `x` to `decimals` places with halves away
from zero, Pine's `math.round(x, n)`. `decimals` is clamped to `0..15`;
`NaN` and the infinities pass through unchanged. `Math.round` follows
JavaScript, where a negative half goes up (`Math.round(-2.5)` is `-2`)
and there is no decimals argument; `roundTo(-2.5, 0)` is `-3`.

| Call | Result |
| --- | --- |
| `roundTo(2.5, 0)` | `3` |
| `roundTo(-2.5, 0)` | `-3` |
| `roundTo(7.125, 2)` | `7.13` |
| `roundTo(1.23456, 3)` | `1.235` |
| `roundTo(x, -1)` | the same as `roundTo(x, 0)` |
| `roundTo(x, 40)` | the same as `roundTo(x, 15)` |
| `roundTo(NaN, 2)` | `NaN` |

The decision reads the fractional part of `abs(x) * 10^decimals` in `f64`
(a fraction of `0.5` or more rounds up, nothing is added first), the
result is that integer divided back with the sign restored, and `x` comes
back unchanged when it already sits at that precision or when `abs(x)` is
`2^52` or more, where every `f64` is an integer. A value whose binary form
sits just under a half, `1.005` at two places, rounds down the way its
`f64` does. Use
`roundTo` for values you compute with, plot or compare; to print a value
at a fixed number of decimals use the text builder's `f64(x, decimals)`
([Strings and text](text-formatting.md)), which formats without rounding
the number you keep.

## Rounding to a tick: `roundToTick`

`roundToTick(x, tick)` rounds `x` to the nearest multiple of `tick`, ties
away from zero as `roundTo` does: Pine's `math.round_to_mintick(x)` with
the tick given. Pine words the ties of `math.round` and
`math.round_to_mintick` the same way ("ties rounding up"), and on
TradingView `math.round(-2.5)` reads `-3` (captured), so the tick form
follows `roundTo` on a negative tie; a negative tie has not been captured
for `math.round_to_mintick` itself, since prices are positive, so that
one case rests on the shared wording. On a chart whose market publishes a tick size the call is
`roundToTick(x, p_market_tick_size())`; where the tick size reads `0`
(the market has only its price decimals) use
`roundTo(x, i32(p_market_price_precision()))` instead. `x` comes back
unchanged when `tick` is not finite or not above `0`, when `x` is `NaN` or
infinite, and when `x` already sits on a tick.

| Call | Result |
| --- | --- |
| `roundToTick(1.125, 0.25)` | `1.25` (a tie, away from zero) |
| `roundToTick(-1.125, 0.25)` | `-1.25` |
| `roundToTick(1.1, 0.25)` | `1` |
| `roundToTick(6.25, 2.5)` | `7.5` |
| `roundToTick(100.08, 0.05)` | `100.1` |
| `roundToTick(x, 0.01)` | the same as `roundTo(x, 2)`, to the bit |
| `roundToTick(x, 0)` | `x` |
| `roundToTick(NaN, 0.5)` | `NaN` |

A tick that is a unit fraction (`0.01`, `0.25`, `0.0001`, `0.5`, `1`: one
over an integer `k`) is handled the way `roundTo` handles decimals, the
decision on the fractional part of `abs(x) * k` in `f64` and the result
that integer divided by `k`, so the answer is the nearest `f64` to the
decimal a trader would write (`100.1`, not `100.10000000000001`). Any
other tick (`2.5`, `5`, `0.3`) rounds `abs(x) / tick` and multiplies back.
The levels in [Percentiles of a list](#percentiles-of-a-list) are rounded
this way, with `roundTo` where the tick reads 0.

## Repeatable random numbers: `Random`

`Random` is a seeded generator for Pine's `math.random(min, max, seed)`:
the same seed gives the same sequence on every host and every run, and
`reseed(seed)` in `onReset()` replays it. Construct it in `onStart()`;
`next()` and `between()` never allocate. Pine's unseeded `math.random()`
differs on every run and there is no clock here to draw a seed from, so
pick one. The generator is TradingView's: a seeded `math.random` there
draws `java.util.Random`'s `nextDouble()` sequence for the seed, and so
does `Random`, draw for draw (seed `42` starts `0.7275636800328681`,
`0.6832234717598454`, `0.30871945533265976`; seed `1` starts
`0.7308781907032909`, `0.41008081149220166`, the values captured on
TradingView). TradingView keeps one sequence per `math.random` call
site, so construct one `Random` per call a Pine script makes.

| Member | Meaning |
| --- | --- |
| `new Random(seed)` | a generator at `seed` (an integer); construct in `onStart()` |
| `next()` | the next value in `[0, 1)`, a multiple of `2^-53`, so `1.0` never comes out; Pine's `math.random()` with the seed |
| `between(min, max)` | `min + (max - min) * next()`, a value in `[min, max)`; Pine's `math.random(min, max)` |
| `reseed(seed)` | restart the sequence from `seed`: the values that follow are those a new `Random(seed)` gives |

A random walk from the first close, the same path on every run: a baseline
to test a signal against.

```typescript sample=fn-stats-random-walk
// A random walk from the first close, up to 1% a bar either way: the same path on every run for one seed, a baseline to test a signal against.
param.int("seed", 42, { min: 1, max: 1000000, label: "Seed" });
output("walk", line, overlay, { color: "#a855f7", description: "A seeded random walk from the first close" });

let random = new Random(42);
let seed: i64 = 42;
let walk: f64 = NaN;

function onStart(): void {
  seed = i64(p_seed());
  random = new Random(seed);
}

function onBar(): void {
  const close = bar.close();
  if (isNaN(close)) return;
  walk = isNaN(walk) ? close : walk * (1.0 + random.between(-0.01, 0.01));
  out_walk(walk);
}

// A reset replays the same path from the seed.
function onReset(): void {
  random.reseed(seed);
  walk = NaN;
}
```

- **One seed, one path.** `between(-0.01, 0.01)` draws one step per bar,
  and seed `42` draws the same steps on every run and every host: the walk's
  first step is `0.7275636800328681` of the way through the range, the
  value TradingView's `math.random` gives for that seed.
- **A reset replays it.** `reseed(seed)` in `onReset()` restarts the
  sequence, so a reset chart draws the same walk again.
- **A baseline.** A signal that scores as well on the walk as on the price
  has found nothing.

## From Pine

One to one: the call returns Pine's value on the same bars. The second
table holds the three `List` calls that differ from Pine's `array.*`.

| Pine | Here |
| --- | --- |
| `array.sum`, `array.avg`, `array.variance`, `array.stdev`, `array.min`, `array.max`, `array.indexof(array.min(a))`, `array.covariance`, `array.sort`, `array.median`, `array.percentile_nearest_rank`, `array.percentile_linear_interpolation` | `stats.sum`, `stats.mean`, `stats.variance`, `stats.stdev`, `stats.min`, `stats.max`, `stats.argmin` (and `argmax`), `stats.covariance`, `stats.sortAscending`, `stats.median`, `stats.percentile`, `stats.percentileLinearInterpolation` over a `StaticArray<f64>` and a count |
| `x[n]` | `new History(size)`, `.push(x)` once per bar, `.ago(n)` |
| `array.new<float>()`, `array.push`, `array.pop`, `array.shift`, `array.unshift`, `array.get`, `array.set`, `array.insert`, `array.remove`, `array.first`, `array.last`, `array.indexof`, `array.includes`, `array.size`, `array.clear` | `new List(capacity)` in `onStart()`, then `.push`, `.pop`, `.shift`, `.unshift`, `.get`, `.set`, `.insert`, `.remove`, `.first`, `.last`, `.indexOf`, `.includes`, `.size`, `.clear` |
| `array.sort(a)`, `array.sort(a, order.descending)` | `list.sort()`, `list.sort(false)` |
| `array.sum`, `array.avg`, `array.min`, `array.max`, `array.stdev` on an array you keep | `list.sum()`, `.mean()`, `.min()`, `.max()`, `.stdev()` |
| `array.from(v0, v1, ...)` | `List.from([v0, v1, ...], capacity)` in `onStart()` |
| `array.copy(a)`, `array.slice(a, from, to)`, `array.abs(a)`, `array.standardize(a)` | `list.copy(into)`, `.slice(into, from, to)`, `.abs(into)`, `.standardize(into)` with `into` a second `List` made in `onStart()` |
| `array.fill(a, v, from, to)`, `array.reverse(a)` | `list.fill(v, from, to)`, `.reverse()` |
| `array.every(a)`, `array.some(a)` | `list.every()`, `.some()` over entries held as `1.0` and `0.0` |
| `array.sort_indices(a, order)` | `list.sortIndices(into)` / `.sortIndices(into, false)` with `into` a `StaticArray<i32>` made in `onStart()` (TradingView's orders: stable ascending with `na` last, that order reversed descending) |
| `array.median(a)`, `array.percentile_nearest_rank(a, pct)`, `array.percentile_linear_interpolation(a, pct)` | `list.median()`, `.percentileNearestRank(pct)`, `.percentileLinearInterpolation(pct)` |
| `if array.size(lines) > N` then `line.delete(array.shift(lines))` | `new HandleRing(N)` in `onStart()`; `const old = ring.push(id); if (old >= 0) lines[old].delete();` |
| `math.round(x, n)` | `roundTo(x, n)` |
| `math.round_to_mintick(x)` | `roundToTick(x, p_market_tick_size())` |
| `math.random(min, max, seed)` | `new Random(seed)` in `onStart()`, then `.between(min, max)` (`.next()` for `math.random()`) |

| Pine | Here | The difference |
| --- | --- | --- |
| `array.push(a, v)` on an array that grows without bound | `list.push(v)` | a `List` has the capacity it was given in `onStart()`: `push` returns `false` when full, and `pushEvict` drops the oldest entry instead, the keep-the-last-N shape most scripts want |
| `array.get(a, i)` | `list.get(i)` | a negative `i` counts from the end here (`get(-1)` is the last entry) and an index outside the list reads `NaN` instead of stopping the script |
| `array.slice(a, from, to)` | `list.slice(into, from, to)` | a copy into `into`, where Pine's slice is a live view of the original; a bound outside the list is clamped instead of stopping the script |
