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), so the
./sdk/levels module keeps the state for you. PeriodLevels
and SessionLevels roll over on the clock from the
clock and sessions kit, 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 Period; .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 Session; .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 Pivot; .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 | round | Fills out (a Static) 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) |
Support | new Support; .update(high, low, close); .count(), .level(i), .touches(i), .kind(i), ., ., .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: 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()andlow()are the running extremes so far.- On the bar a new period starts, the period that just ended becomes
prevOpen(),prevHigh(),prevLow()andprevClose()(the close of its last bar). They hold for the whole period and never repaint. prev*areNaNuntil the first period boundary after construction orreset(). 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 recipe: on an intraday chart the firstprev*may describe a partial day.- A non-finite
openmakesopen()NaNfor the period; a non-finitehighorlowmakes that extremeNaNfor the rest of the period (and theprev*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:
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()andlow()describe the instance in progress and readNaNoutside 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()andprevClose(); they hold until the next instance ends, so the previous session's range is available all night and through the weekend. prev*areNaNuntil one instance has ended after construction orreset(); 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():
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 ==.
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) or keep one zone per side
(Zone tracker); 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), 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
tolerancePctpercent of the pivot's price: the level's price becomes the touch-weighted mean of everything that merged into it and itstouchesgrow by one. A tolerance of0merges 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:
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 thePeriodLevelsbuilt from that word, orclock.isNewDay()and its siblings on aClock(clock and sessions kit).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 fromPeriodLevelsas its arguments.