---
title: "Special indicators"
description: "Four tools that do not fit the average-or-oscillator mold. VWAP tracks the volume-weighted fair price and can reset on a calendar anchor. Ichimoku bundles five…"
order: 35
section: "functions"
---

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

# Special indicators

Four tools that do not fit the average-or-oscillator mold. VWAP tracks
the volume-weighted fair price and can reset on a calendar anchor.
Ichimoku bundles five trend components, two of them projected ahead.
Supertrend is an ATR trailing stop that knows which side of the market it
is on. PSAR is a parabolic stop-and-reverse dot. All four ship as classes
in `src/sdk/ta.ts` (`import { Ichimoku, Psar, Supertrend, Vwap } from
"./sdk/ta";`), each one the kScript (legacy) builtin it replaces, checked
bar for bar against the kScript engine. The engine conventions each class
keeps are named in its section; the [TA library](ta-library.md) page has
the whole catalog and the accuracy contract.

## Vwap

**Ships as `Vwap`.** kScript's `vwap(anchor?, price?)` is a
reset-on-boundary sum: price times volume over volume, accumulated from
an anchor. `new Vwap(anchor, price)` takes the anchor as a string (`""`
for none, `"day"`, `"week"`, `"month"`, `"quarter"`, `"year"`, or a
number of milliseconds such as `"14400000"` for fixed four-hour buckets)
and the bar price as a string (`"hlc3"` by default, or `"hl2"`, `"ohlc4"`,
`"hlcc4"`, `"close"`). `update(open, high, low, close, volume, tsMs)`
returns the running VWAP; `tsMs` is the bar's open time in milliseconds
since the epoch and is only read when an anchor is set. The `time` source
hands the module epoch seconds (`input("bar_t", time.bar_open_sec)`), so
multiply by `1000.0` before passing it.

| Anchor | Boundary | First finite value |
| --- | --- | --- |
| `""` (none) | never; accumulates from the first loaded bar | the first bar |
| `"day"` | every 00:00 UTC | the first bar |
| `"week"` | every Monday 00:00 UTC | the first bar |
| `"month"`, `"quarter"`, `"year"` | the first of the period, 00:00 UTC | the first bar |
| `"14400000"` (any number) | every bucket of that many milliseconds, floored from the epoch | the first bar |

**No leading gap.** The engine starts a bucket on the first bar of the
series whatever the calendar says, so an anchored line is finite from bar
0 and its first period is partial: inside that first period the level
depends on where the loaded history starts, and from the first boundary
on every period computes only from its own bars. The no-anchor form is
partial for the whole series (two charts with different history depths
disagree), which is why the anchored forms are the ones to share.

**Engine conventions.** A bar whose high, low, close or volume is not
finite (or open, for `"ohlc4"`) marks the current bucket invalid: the
value is `NaN` from that bar until the next bucket starts, forever in the
no-anchor form. A zero total volume is `NaN`. A `NaN` time with an anchor
set clears the sums and returns `NaN`. The no-anchor, `"day"` and
numeric-millisecond anchors match the engine bit for bit on the reference
window; `"week"`, `"month"`, `"quarter"` and `"year"` are ported from the
same calendar arithmetic but unproven on that window. Not mirrored, because
a per-bar class never sees the data: the engine's session-calendar
bucketing on venues with a trading calendar, its regular-trading-hours
filter, and the extra history it loads for quarter and year anchors.

```typescript
import { in_bar_t, in_close, in_high, in_low, in_open, in_volume } from "./gen/inputs";
import { Vwap } from "./sdk/ta";

const weekly = new Vwap("week");             // hlc3 price, resets every Monday 00:00 UTC
const session = new Vwap("14400000", "close"); // four-hour buckets over the close

// In state(): the time source is epoch seconds, the class wants milliseconds.
const tMs = in_bar_t() * 1000.0;
const weekValue = weekly.update(in_open(), in_high(), in_low(), in_close(), in_volume(), tMs);
```

The anchor is a bucket id computed from the bar's open time: a UTC day
index for `"day"`, a week index whose origin is three days before the
epoch (so weeks start on Monday) for `"week"`, and civil calendar math for
the month, quarter and year forms. When the id changes the sums reset:

```typescript
// The same rule the class applies, written out for a custom anchor.
const DAY_MS: f64 = 86400000.0;
const dayId = Math.floor(tMs / DAY_MS);                  // "day"
const weekId = Math.floor((tMs + 3.0 * DAY_MS) / (7.0 * DAY_MS)); // "week", Monday origin
const bucketId = Math.floor(tMs / 14400000.0);            // "14400000"
```

Assign the class to a module-level `let` like any other; there is nothing
to wait for, the line is finite on the first bar with volume.

## Ichimoku

**Ships as `Ichimoku`.** `new Ichimoku(conversionPeriod, basePeriod,
laggingSpanPeriod, displacement)` with the kScript defaults `9`, `26`,
`52`, `26`; `update(high, low, close)` returns `tenkan` and sets the
fields `tenkan`, `kijun`, `senkouA`, `senkouB`, `chikou`. Each line is the
midpoint of the highest high and lowest low over its period. The engine
conventions the class keeps:

- **Partial windows, no warm-up.** Bar 0 already has a value from its own
  bar; there is no `NaN` lead-in. A `NaN` high or low inside the window is
  skipped, but the current bar's own `NaN` poisons the value, and every
  output that comes out `NaN` is reported as `0` (the engine never returns
  na from `ichimoku`).
- **The displacement is inside the math.** `senkouA` on bar `i` is
  `(tenkan + kijun) / 2` as it stood `displacement` bars earlier, and
  `senkouB` is the 52-bar midpoint from `displacement` bars earlier; on
  the first `displacement` bars both fall back to the current bar's
  values. The two fields are therefore already the cloud that belongs on
  the current bar, so write them to plain outputs with no
  `displacement_bars`. The projection past the newest loaded bar is not
  emitted, exactly as in the engine.
- **`chikou` is the current close.** The engine reads the close of bar
  `i + displacement`, a future bar, and only falls back to the current
  close on the last `displacement` bars of the series; a class that sees
  one bar at a time cannot read ahead, so the field is the current close.
  Declaring `displacement_bars: -26` on its output draws that close 26
  bars back, which is the lagging span as the engine's chart shows it.
  `om chart indicator preview` refuses a displaced output by name
  (`wrun_preview_displacement_unsupported`), so preview the other lines.

```typescript
import { in_close, in_high, in_low } from "./gen/inputs";
import { Ichimoku } from "./sdk/ta";

const cloud = new Ichimoku(9, 26, 52, 26);

// In state(): one update per bar, then read the five lines by name.
cloud.update(in_high(), in_low(), in_close());
const bullish = cloud.senkouA >= cloud.senkouB; // the cloud on this bar, already shifted
```

The cloud itself is two boxes: `box("cloud_up", { top: senkouA, bottom:
senkouB, when: aAbove })` draws, for each bar, a one-bar slice between the
two spans the class reports for that bar, and a second box gated the other
way draws the bearish slices in the other color.

## Supertrend

**Ships as `Supertrend`.** `new Supertrend(factor, atrPeriod)` (kScript
has no defaults; `3` and `10` are the usual values); `update(high, low,
close)` returns the stop and sets `line` and `direction` (`1` up trend,
the line sits below price; `-1` down trend, the line sits above). Bands
sit `factor` average true ranges either side of the bar midpoint and
ratchet toward price; the stop is the band on the far side of price, and
the regime flips when the close trades through it. The ATR is the same
Wilder smoothing as `Atr`, so both fields are `NaN` for the first
`atrPeriod - 1` bars, and a bar whose ATR is `NaN` after that yields `NaN`
without touching the band state. Conventions in full on the
[Trend indicators](trend-indicators.md) page; the module below uses
`direction` to drive a `color_by` ladder on the line.

## Psar

**Ships as `Psar`.** `new Psar(start, increment, maxValue)` with the
defaults `0.02`, `0.02`, `0.2`; `update(high, low, close)` returns the
SAR price, which jumps to the other side of price when the trend flips.
Bar 0 is `NaN`; bar 1 picks the opening trend from `close[1] >= close[0]`
and returns the first SAR; the close is only read on those two bars. There
is no direction field: the trend side is `close > sar` on the bar, or
compare the SAR to the low. A non-finite bar makes the SAR `NaN` for the
rest of the series, as in the engine. Conventions in full on the
[Trend indicators](trend-indicators.md) page.

## Putting them together: VWAP anchors

The four VWAP variants side by side. Every line is finite from the first
bar; the anchored ones reset at their boundary and are partial before the
first one. `day_start` is a data-only flag that is `1` on the bar that
opens a new UTC day, the bar where the daily sums reset, so the boundary is
readable as a metric too.

```typescript
import { input, line, none, ohlcv, output, overlay, time } from "./sdk/declare";
import { in_bar_t, in_close, in_high, in_low, in_open, in_volume } from "./gen/inputs";
import { emitRow, out_day_start, out_vwap_cum, out_vwap_day, out_vwap_month, out_vwap_week } from "./gen/outputs";
import { Vwap } from "./sdk/ta";

input("open", ohlcv.open);
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);
output("vwap_cum", line, overlay, { color: "#2563eb", width: 2, description: "VWAP with no anchor, from the first loaded bar" });
output("vwap_day", line, overlay, { color: "#16a34a", width: 2, description: "VWAP anchored to the UTC day" });
output("vwap_week", line, overlay, { color: "#f97316", width: 2, description: "VWAP anchored to the UTC week" });
output("vwap_month", line, overlay, { color: "#7c3aed", width: 2, description: "VWAP anchored to the UTC month" });
output("day_start", none, overlay, { description: "1 on the bar that opens a new UTC day" });

const DAY_MS: f64 = 86400000.0;
const cumulative = new Vwap("");
const daily = new Vwap("day");
const weekly = new Vwap("week");
const monthly = new Vwap("month");
let prevDay: f64 = NaN;
let dayStart: f64 = 0.0;
let cumValue: f64 = NaN;
let dayValue: f64 = NaN;
let weekValue: f64 = NaN;
let monthValue: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  const open = in_open();
  const high = in_high();
  const low = in_low();
  const close = in_close();
  const volume = in_volume();
  const tMs = in_bar_t() * 1000.0; // the time source is seconds; the class wants milliseconds
  const day = Math.floor(tMs / DAY_MS);
  dayStart = !isNaN(prevDay) && day != prevDay ? 1.0 : 0.0;
  prevDay = day;
  cumValue = cumulative.update(open, high, low, close, volume, tMs);
  dayValue = daily.update(open, high, low, close, volume, tMs);
  weekValue = weekly.update(open, high, low, close, volume, tMs);
  monthValue = monthly.update(open, high, low, close, volume, tMs);
  return 1;
}

export function finalize(): void {
  out_vwap_cum(cumValue);
  out_vwap_day(dayValue);
  out_vwap_week(weekValue);
  out_vwap_month(monthValue);
  out_day_start(dayStart);
  emitRow();
}

export function reset(): void {
  cumulative.reset();
  daily.reset();
  weekly.reset();
  monthly.reset();
  prevDay = NaN;
  dayStart = 0.0;
  cumValue = NaN;
  dayValue = NaN;
  weekValue = NaN;
  monthValue = NaN;
}
```

## Putting them together: cloud, stop, and dots

Ichimoku with its two-color cloud and the lagging span drawn back, the
Supertrend stop colored by regime, and the SAR as dots, in one overlay.

```typescript
import { box, input, line, none, ohlcv, output, overlay, param, scatter } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import {
  emitRow,
  out_a_above,
  out_b_above,
  out_chikou,
  out_kijun,
  out_psar,
  out_senkou_a,
  out_senkou_b,
  out_st_dir,
  out_st_line,
  out_tenkan,
} from "./gen/outputs";
import { p_atr_period, p_factor } from "./gen/params";
import { Ichimoku, Psar, Supertrend } from "./sdk/ta";

param("factor", 3, { min: 0.5, max: 10, description: "Supertrend ATR multiplier" });
param("atr_period", 10, { min: 1, max: 200, description: "Supertrend ATR window" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
output("tenkan", line, overlay, { color: "#0891b2", width: 1, description: "Conversion line, 9-bar midpoint" });
output("kijun", line, overlay, { color: "#be123c", width: 1, description: "Base line, 26-bar midpoint" });
// The spans are already shifted inside the class: the value on a bar is the cloud for that bar.
const senkouA = output("senkou_a", line, overlay, { color: "#0f766e", width: 1, description: "Leading span A, the class shifts it 26 bars ahead" });
const senkouB = output("senkou_b", line, overlay, { color: "#b45309", width: 1, description: "Leading span B, the class shifts it 26 bars ahead" });
output("chikou", line, overlay, { color: "#64748b", width: 1, displacement_bars: -26, description: "Lagging span, the close drawn 26 bars back" });
const aAbove = output("a_above", none, overlay, { description: "1 where span A is above span B: the bullish cloud gate" });
const bAbove = output("b_above", none, overlay, { description: "1 where span B is above span A: the bearish cloud gate" });
// The cloud: one slice per bar between the two spans, tinted by which span is on top.
box("cloud_up", { top: senkouA, bottom: senkouB, when: aAbove, color: "#16a34a", opacity: 0.12, borderWidth: 0 });
box("cloud_down", { top: senkouB, bottom: senkouA, when: bAbove, color: "#dc2626", opacity: 0.12, borderWidth: 0 });
output("st_line", line, overlay, { width: 2, color_by: "st_dir", colors: ["#dc2626", "#16a34a"], description: "Supertrend stop, colored by regime" });
output("st_dir", none, overlay, { description: "0 short, 1 long" });
output("psar", scatter, overlay, { color: "#9333ea", description: "Parabolic SAR" });

let cloud = new Ichimoku(9, 26, 52, 26);
let st = new Supertrend(3.0, 10);
let psar = new Psar(0.02, 0.02, 0.2);
let psarValue: f64 = NaN;

export function init(): void {
  cloud = new Ichimoku(9, 26, 52, 26);
  st = new Supertrend(p_factor(), i32(p_atr_period()));
  psar = new Psar(0.02, 0.02, 0.2);
}

export function state(): i32 {
  const close = in_close();
  const high = in_high();
  const low = in_low();
  cloud.update(high, low, close);
  st.update(high, low, close);
  psarValue = psar.update(high, low, close);
  return 1;
}

export function finalize(): void {
  out_tenkan(cloud.tenkan);
  out_kijun(cloud.kijun);
  out_senkou_a(cloud.senkouA);
  out_senkou_b(cloud.senkouB);
  out_chikou(cloud.chikou);
  out_a_above(cloud.senkouA >= cloud.senkouB ? 1.0 : 0.0);
  out_b_above(cloud.senkouB > cloud.senkouA ? 1.0 : 0.0);
  out_st_line(st.line);
  out_st_dir(st.direction > 0.0 ? 1.0 : 0.0);
  out_psar(psarValue);
  emitRow();
}

export function reset(): void {
  cloud.reset();
  st.reset();
  psar.reset();
  psarValue = NaN;
}
```

## Seeing the VWAP anchor boundary

To watch exactly where each anchored line resets, write a flag that is
`1` on the bar whose bucket id differs from the previous bar's, the way
`day_start` does above for the daily line. Because the flag is an
output, `om metric series` on it shows the boundary bars without a chart:

```bash
om metric series --metric wrun/@you/vwap-anchors/day_start --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 200
```

## What changed from kScript

- `vwap(anchor="week")` is `new Vwap("week")` fed the bar's open time in
  milliseconds; the anchor is a bucket id from the `time` source, and the
  same reset-on-id rule is a few lines of arithmetic for any anchor the
  class does not name (a session open, a news bar, a manual level).
- Ichimoku's two spans come out of the class already shifted, so their
  outputs carry no `displacement_bars`; the lagging span is the current
  close with `displacement_bars: -26` on its output, and the cloud is two
  gated one-bar boxes.
- Supertrend's `.line` / `.direction` streams are the fields of one
  object, and the direction-colored line is a `color_by` ladder over a
  data-only output instead of a `colorIndex` expression.
- PSAR reads `high`, `low` and `close` as declared inputs; nothing reads
  the chart's OHLC implicitly.
