Stats, history and lists
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
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 Static and a count |
| The ids of the newest N drawings, so the oldest can be deleted | HandleRing |
math.round(x, n), math., math. | 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. | 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. | the population covariance of the two arrays' first n entries |
stats. | 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. | 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. | 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. | 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( 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. and ta.) |
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.
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).
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)
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.
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 Static: entries 0..size() - 1 are the list, so stats. 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( | 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. is false there) |
sort | write into the Static 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 |
percentile | 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 |
percentile | 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.
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.
// 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 lastkeepcloses. - Percentiles and the median.
percentileLinearInterpolation(90)is Pine'sarray.percentile_linear_interpolation(a, 90), andmedian()isarray.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 intoorder, aStaticArray<i32>made inonStart(). 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). "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 Handle | 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:
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 |
round | 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), 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 |
|---|---|
round | 1.25 (a tie, away from zero) |
round | -1.25 |
round | 1 |
round | 7.5 |
round | 100.1 |
roundToTick(x, 0.01) | the same as roundTo(x, 2), to the bit |
roundToTick(x, 0) | x |
round | 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 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. |
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.
// 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 seed42draws the same steps on every run and every host: the walk's first step is0.7275636800328681of the way through the range, the value TradingView'smath.randomgives for that seed. - A reset replays it.
reseed(seed)inonReset()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., array., array.sort, array.median, array., array. | stats.sum, stats.mean, stats.variance, stats.stdev, stats.min, stats.max, stats.argmin (and argmax), stats., stats., stats.median, stats., stats. over a Static and a count |
x[n] | new History(size), .push(x) once per bar, .ago(n) |
array., 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. | list.sort(), list. |
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. | list.copy(into), .slice(into, from, to), .abs(into), . with into a second List made in onStart() |
array.fill(a, v, from, to), array. | 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. | list. / . with into a Static made in onStart() (TradingView's orders: stable ascending with na last, that order reversed descending) |
array.median(a), array., array. | list.median(), ., . |
if array. then line. | new HandleRing(N) in onStart(); const old = ring.push(id); if (old >= 0) lines[old]. |
math.round(x, n) | roundTo(x, n) |
math. | roundToTick(x, p_ |
math. | 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. | 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 |