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…

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 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.

AnchorBoundaryFirst finite value
"" (none)never; accumulates from the first loaded barthe first bar
"day"every 00:00 UTCthe first bar
"week"every Monday 00:00 UTCthe first bar
"month", "quarter", "year"the first of the period, 00:00 UTCthe first bar
"14400000" (any number)every bucket of that many milliseconds, floored from the epochthe 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.

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:

// 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.
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 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 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.

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.

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:

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.