---
title: "Levels kit"
description: "Levels are the prices a trader draws before the session starts: today's open, yesterday's high and low, last week's close, the session's range, the pivot point…"
order: 56
section: "functions"
---

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

# Levels kit

Levels are the prices a trader draws before the session starts: today's
open, yesterday's high and low, last week's close, the session's range,
the pivot point with its supports and resistances, the round numbers
near price, and the swing levels price keeps returning to. A wrun
indicator sees one bar at a time
([Execution model](../core-concepts/execution-model.md)), so the
`./sdk/levels` module keeps the state for you. `PeriodLevels`
and `SessionLevels` roll over on the clock from the
[clock and sessions kit](time-and-sessions-kit.md), `PivotPoints` computes
the standard, Fibonacci, Camarilla and Woodie sets from a previous
range, `roundLevels` fills a grid of round numbers around a price, and
`SupportResistance` clusters confirmed pivots into levels and counts how
often each was touched. Everything is a class or a function you
construct in `onStart()`, feed once per bar and read on the same bar; no
call allocates after construction, so the module never grows memory per
bar.

| Export | Signature | Definition |
| --- | --- | --- |
| `PeriodLevels` | `new PeriodLevels(period, zone = "UTC")`; `.update(t, open, high, low, close)`; `.isNew()`, `.open()`, `.high()`, `.low()`, `.prevOpen()`, `.prevHigh()`, `.prevLow()`, `.prevClose()`, `.reset()` | The running open, high and low of the current `"day"`, `"week"`, `"month"`, `"quarter"` or `"year"` in a time zone (or the period as Pine spells the timeframe: `"D"` / `"1D"`, `"W"` / `"1W"`, `"M"` / `"1M"`, `"3M"`, `"12M"`), and the open, high, low and close of the last completed one (`NaN` until one completes) |
| `SessionLevels` | `new SessionLevels(spec, zone = "UTC", days = "0123456")`; `.update(t, open, high, low, close)`; `.isIn()`, `.isNew()`, `.open()`, `.high()`, `.low()`, `.prevOpen()`, `.prevHigh()`, `.prevLow()`, `.prevClose()`, `.reset()` | The same over an `"HHMM-HHMM"` session on the listed weekdays (`0` = Sunday): the running levels while the bar is inside a session instance, the last completed instance as `prev*` |
| `PivotPoints` | `new PivotPoints(kind = "standard")`; `.compute(high, low, close, open = NaN)`; `.pp()`, `.r1()`, `.r2()`, `.r3()`, `.s1()`, `.s2()`, `.s3()` | The seven floor-trader levels from one range in the `"standard"`, `"fibonacci"`, `"camarilla"` or `"woodie"` form; every `compute()` overwrites them |
| `roundLevels` | `roundLevels(price, step, out): i32` | Fills `out` (a `StaticArray<f64>`) with the multiples of `step` around `price`, the first half at or below it and the rest above, nearest first; returns the count written (`0` for a step of `0` or below, a `NaN` step or a `NaN` price) |
| `SupportResistance` | `new SupportResistance(left, right, tolerancePct, maxLevels)`; `.update(high, low, close)`; `.count()`, `.level(i)`, `.touches(i)`, `.kind(i)`, `.nearestAbove(price)`, `.nearestBelow(price)`, `.reset()` | Every confirmed `PivotHigh` / `PivotLow` merges into the nearest level within `tolerancePct` of its price or starts one; at most `maxLevels` are kept, the least touched dropped first |

The first argument of `update()` on the two calendar classes is the
bar's open time in UTC seconds: pass `bar.time()`. A `period`, `kind`,
zone or session spec the module does not know aborts in the
constructor, which is why every example constructs in `onStart()`: an
abort there shows its message in the Console, one at module start does
not.

## Previous day, week and month levels

`PeriodLevels(period, zone)` follows the `Clock` of the
[clock and sessions kit](time-and-sessions-kit.md): a period is the run
of bars sharing the clock's day, week (Monday start), month, quarter
(from January, April, July or October 1st) or year key in the zone you
name (`"UTC"` by default; `"America/New_York"`, `"Europe/London"` and
the other names that page lists, or a fixed `"+05:30"`). `period` is one
of the five words `"day"`, `"week"`, `"month"`, `"quarter"` and `"year"`,
or the timeframe string a Pine script passes to `request.security`:
`"D"` or `"1D"`, `"W"` or `"1W"`, `"M"` or `"1M"`, `"3M"` for the quarter
and `"12M"` for the year. Anything else (a lowercase `"d"`, an intraday
`"60"`, a `"2D"` the clock has no key for) aborts in the constructor. On
each bar:

- `isNew()` is true on the first bar of a period, and on the very first
  bar the object sees.
- `open()` is the open of the period's first bar; `high()` and `low()`
  are the running extremes so far.
- On the bar a new period starts, the period that just ended becomes
  `prevOpen()`, `prevHigh()`, `prevLow()` and `prevClose()` (the close of
  its last bar). They hold for the whole period and never repaint.
- `prev*` are `NaN` until the first period boundary after construction
  or `reset()`. The period already running when the history starts
  counts as a period, however much of it the loaded bars cover, the
  same rule as the [Key levels](../cookbook/key-levels.md) recipe: on an
  intraday chart the first `prev*` may describe a partial day.
- A non-finite `open` makes `open()` `NaN` for the period; a non-finite
  `high` or `low` makes that extreme `NaN` for the rest of the period
  (and the `prev*` it becomes); `prevClose()` is the last bar's close.

The previous New York day's high and low as lines, with the running
day open dashed and a data-only flag on the first bar of each day:

```typescript sample=fn-levels-kit-previous-day
output("pdh", line, overlay, { color: "#f5a623", width: 1, description: "Previous New York day high" });
output("pdl", line, overlay, { color: "#f5a623", width: 1, description: "Previous New York day low" });
output("day_open", line, overlay, { color: "#94a3b8", width: 1, line_style: "dashed", description: "Open of the running day" });
output("new_day", none, overlay, { description: "1 on the first bar of a New York day" });

let day = new PeriodLevels("day");

function onStart(): void {
  // Construct in onStart(): an unknown period or zone aborts with a readable message here.
  day = new PeriodLevels("day", "America/New_York");
}

function onBar(): void {
  day.update(bar.time(), bar.open(), bar.high(), bar.low(), bar.close());
  out_pdh(day.prevHigh());
  out_pdl(day.prevLow());
  out_day_open(day.open());
  out_new_day(day.isNew() ? 1.0 : 0.0);
}
```

The two level lines are `NaN` until the first New York midnight in the
loaded history, then step on each midnight, daylight time included: the
clock handles the 23-hour and 25-hour days. Four more `PeriodLevels`
objects give the week, the month, the quarter and the year, or the same
day in another zone; a yearly open is `new PeriodLevels("12M", zone)`
read with `open()`.

## Session levels

`SessionLevels(spec, zone, days)` follows the `Session` of the same kit:
`spec` is `"HHMM-HHMM"` in the zone's local time, half-open (`"0930-1600"`
holds 09:30 and excludes 16:00) and wrapping past midnight when the
close is earlier than the open; `days` lists the weekday digits of the
instance's opening day, `0` = Sunday through `6` = Saturday, so
`"12345"` is Monday to Friday. Pine's session day string `"23456"`
(Sunday = 1) is `"12345"` here.

- `isIn()` is true while the bar sits inside an instance; `isNew()` on
  its first in-session bar.
- `open()`, `high()` and `low()` describe the instance in progress and
  read `NaN` outside a session.
- On the first bar after an instance ended (outside the hours, or
  already the first bar of the next instance when the bars skip the
  close) the ended instance becomes `prevOpen()`, `prevHigh()`,
  `prevLow()` and `prevClose()`; they hold until the next instance ends,
  so the previous session's range is available all night and through
  the weekend.
- `prev*` are `NaN` until one instance has ended after construction or
  `reset()`; an instance already running when the history starts counts
  from its first loaded bar. A session no bar ever opens inside (a daily
  chart against an intraday session) never produces levels.

`new SessionLevels("0930-1600", "America/New_York", "12345")` is the
regular trading session of the US stock exchanges; feed it exactly as
the example above feeds `PeriodLevels` and draw `prevHigh()` and
`prevLow()` the same way.

## Pivot points

`PivotPoints(kind).compute(high, low, close)` takes one range, normally
the previous day's from `PeriodLevels`, and fills seven levels. With
`H`, `L`, `C` and the range `R = H - L`:

| Kind | Pivot | Resistances | Supports |
| --- | --- | --- | --- |
| `"standard"` | `PP = (H + L + C) / 3` | `R1 = 2 PP - L`, `R2 = PP + R`, `R3 = H + 2 (PP - L)` | `S1 = 2 PP - H`, `S2 = PP - R`, `S3 = L - 2 (H - PP)` |
| `"fibonacci"` | `PP` as standard | `R1 = PP + 0.382 R`, `R2 = PP + 0.618 R`, `R3 = PP + R` | `S1 = PP - 0.382 R`, `S2 = PP - 0.618 R`, `S3 = PP - R` |
| `"camarilla"` | `PP` as standard | `R1 = C + R * 1.1 / 12`, `R2 = C + R * 1.1 / 6`, `R3 = C + R * 1.1 / 4` | `S1 = C - R * 1.1 / 12`, `S2 = C - R * 1.1 / 6`, `S3 = C - R * 1.1 / 4` |
| `"woodie"` | `PP = (H + L + 2 C) / 4` | the standard formulas from that `PP` | the standard formulas from that `PP` |

`compute()` accepts a fourth `open` argument for kinds that read it;
none of the four does. A non-finite high, low or close makes all seven
levels `NaN`, and there is no `reset()`: every `compute()` overwrites
every level, so `compute(NaN, NaN, NaN)` clears them. The kind is chosen
at construction; to make it a setting, map a numeric param to the name
in `onStart()`:

```typescript sample=fn-levels-kit-pivot-points
param("kind", 0, { min: 0, max: 3, description: "0 standard, 1 fibonacci, 2 camarilla, 3 woodie" });
output("pp", line, overlay, { color: "#94a3b8", width: 1, description: "Pivot point from the previous UTC day" });
output("r1", line, overlay, { color: "#dc2626", width: 1, description: "First resistance" });
output("r2", line, overlay, { color: "#dc2626", width: 1, line_style: "dashed", description: "Second resistance" });
output("r3", line, overlay, { color: "#dc2626", width: 1, line_style: "dotted", description: "Third resistance" });
output("s1", line, overlay, { color: "#16a34a", width: 1, description: "First support" });
output("s2", line, overlay, { color: "#16a34a", width: 1, line_style: "dashed", description: "Second support" });
output("s3", line, overlay, { color: "#16a34a", width: 1, line_style: "dotted", description: "Third support" });

function kindName(kind: i32): string {
  if (kind == 1) return "fibonacci";
  if (kind == 2) return "camarilla";
  if (kind == 3) return "woodie";
  return "standard";
}

let day = new PeriodLevels("day");
let pivots = new PivotPoints();

function onStart(): void {
  day = new PeriodLevels("day");
  pivots = new PivotPoints(kindName(i32(p_kind())));
}

function onBar(): void {
  day.update(bar.time(), bar.open(), bar.high(), bar.low(), bar.close());
  // The previous day's range stands for the whole day: recompute once, when the day turns.
  if (day.isNew()) pivots.compute(day.prevHigh(), day.prevLow(), day.prevClose());
  out_pp(pivots.pp());
  out_r1(pivots.r1());
  out_r2(pivots.r2());
  out_r3(pivots.r3());
  out_s1(pivots.s1());
  out_s2(pivots.s2());
  out_s3(pivots.s3());
}
```

Weekly pivots are the same file with `new PeriodLevels("week")` (or
`"W"`, as Pine spells it); a session's pivots feed `compute()` from
`SessionLevels` on the bar its `prev*` change.

## Round numbers

`roundLevels(price, step, out)` fills a `StaticArray<f64>` you allocated
once (at module level or in `onStart()`) with the multiples of `step`
around `price`: with `base` the nearest multiple at or below `price`,
the first `out.length / 2` slots (integer division) hold `base`,
`base - step`, `base - 2 step`, ... and the remaining slots hold
`base + step`, `base + 2 step`, ..., each half nearest first. It returns
how many slots it wrote, `out.length` on success. A step of `0` or
below, a `NaN` step or a `NaN` price write `NaN` into every slot and
return `0`, so a stale grid never survives a bad bar. The values are
floating-point products (`3 * 0.1` prints as `0.30000000000000004`):
format them for display instead of comparing them with `==`.

```typescript
const grid = new StaticArray<f64>(6); // 3 at or below the close, 3 above

// In onBar(): grid[0] is the nearest round number at or below the close,
// grid[3] the nearest above; count is 6, or 0 when the step was unusable.
// const count = roundLevels(bar.close(), 100.0, grid);
```

A step of `100` on a five-figure price gives the hundreds; derive it
from the price instead (`Math.pow(10.0, Math.floor(Math.log10(close)) - 1)`)
for a grid that scales with the market.

## Clustered support and resistance

`SupportResistance(left, right, tolerancePct, maxLevels)` is new: the
shipped recipes take calendar extrema
([Key levels](../cookbook/key-levels.md)) or keep one zone per side
([Zone tracker](../cookbook/zone-tracker.md)); this class keeps a small
ledger of levels built from every confirmed swing and remembers how
often each was hit. It runs a `PivotHigh(left, right)` over the high
and a `PivotLow(left, right)` over the low from `./sdk/ta`
([Series functions](series-functions.md)), so a pivot is reported
`right` bars after its bar and never repaints. On each confirmed pivot:

- It merges into the NEAREST existing level whose price is within
  `tolerancePct` percent of the pivot's price: the level's price becomes
  the touch-weighted mean of everything that merged into it and its
  `touches` grow by one. A tolerance of `0` merges exact matches only.
- Otherwise it starts a new level with one touch. When that would exceed
  `maxLevels`, the existing level with the fewest touches is dropped
  first, the oldest on a tie; a new level always enters.
- When a high and a low confirm on the same bar, the high merges first.

`count()` is how many levels are kept, `level(i)` the price of the
`i`-th by age (`0` the oldest kept), `touches(i)` its count and
`kind(i)` `+1` when more pivot highs than lows merged into it
(resistance), `-1` when more lows (support), the latest pivot's side on
a tie; an index outside `0..count() - 1` reads `NaN`, `0` and `0`.
`nearestAbove(price)` is the lowest level strictly above `price` and
`nearestBelow(price)` the highest strictly below, `NaN` when there is
none. `update(high, low, close)` takes the close for symmetry with the
other kit classes and does not read it. `reset()` empties the ledger
and the pivot windows.

The levels as line handles, one per kept level, coloured by side and
extended to the right, with the nearest level above and below the close
as ordinary lines:

```typescript sample=fn-levels-kit-support-resistance
param("left", 5, { min: 1, max: 50, description: "Bars a swing must dominate on its left" });
param("right", 5, { min: 1, max: 50, description: "Bars that confirm the swing on its right" });
param("tolerance_pct", 0.3, { min: 0, max: 5, description: "Merge band as a percent of the pivot's price" });
param("max_levels", 6, { min: 1, max: 12, description: "Levels kept; the least touched goes first" });
output("nearest_above", line, overlay, { color: "#dc2626", width: 1, description: "The nearest level above the close" });
output("nearest_below", line, overlay, { color: "#16a34a", width: 1, description: "The nearest level below the close" });
output("level_count", none, overlay, { description: "Levels kept on this bar" });
handles.line({ color: "#94a3b8", width: 1, lineStyle: "dashed", extend: "right" });

let sr = new SupportResistance(5, 5, 0.3, 6);
const lines = new Array<LineHandle>();
let firstT: f64 = NaN;

function onStart(): void {
  sr = new SupportResistance(i32(p_left()), i32(p_right()), p_tolerance_pct(), i32(p_max_levels()));
  // One handle per kept level, made once here: ids are one space across the handle kinds.
  for (let i = 0; i < i32(p_max_levels()); i++) lines.push(draw.line(i));
}

function onBar(): void {
  const t = bar.time();
  if (isNaN(firstT)) firstT = t;
  const close = bar.close();
  sr.update(bar.high(), bar.low(), close);
  const count = sr.count();
  for (let i = 0; i < lines.length; i++) {
    if (i < count) {
      // Levels move as pivots merge, so every bar re-sets each line: same id, same line.
      const price = sr.level(i);
      const ink = sr.kind(i) > 0 ? rgba(220, 38, 38, 255) : rgba(22, 163, 74, 255);
      lines[i].set(firstT, price, t, price).color(ink).width(sr.touches(i) > 1 ? 2.0 : 1.0);
    } else {
      // A slot without a level: delete() on an id nobody holds is a no-op.
      lines[i].delete();
    }
  }
  out_nearest_above(sr.nearestAbove(close));
  out_nearest_below(sr.nearestBelow(close));
  out_level_count(f64(count));
}
```

Levels with more than one touch draw thicker. Because a level's index
is its age, a level that merges keeps its slot and its line; when a
level is dropped the younger levels move down one index and the new
level takes the last one, so several lines can change price on that
bar. Read `last 20 level_count` at the editor's Console prompt to see
the ledger fill, and `nearest_above` or `nearest_below` for the level
price itself.

## From Pine

- `request.security(syminfo.tickerid, "D", high[1])`: `new PeriodLevels("D")` (the same object as `"day"`) fed each bar, then `.prevHigh()` (and `.prevLow()`, `.prevOpen()`, `.prevClose()` for the other fields).
- `request.security(syminfo.tickerid, "W", close[1])`, and the same with `"M"`, `"3M"` or `"12M"`: `new PeriodLevels("W")`, `("M")`, `("3M")`, `("12M")` and `.prevClose()`; the same request without `[1]` is the running period, `.open()`, `.high()` and `.low()`.
- `timeframe.change("D")` (or `"W"`, `"M"`, `"3M"`, `"12M"`): `.isNew()` on the `PeriodLevels` built from that word, or `clock.isNewDay()` and its siblings on a `Clock` ([clock and sessions kit](time-and-sessions-kit.md)).
- `ta.pivot_point_levels(type, anchor)`: `new PivotPoints(kind)` with `.compute()` called on the bar the anchor period turns, the previous period's high, low and close from `PeriodLevels` as its arguments.
