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.
| 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.
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
NaNlead-in. ANaNhigh or low inside the window is skipped, but the current bar's ownNaNpoisons the value, and every output that comes outNaNis reported as0(the engine never returns na fromichimoku). - The displacement is inside the math.
senkouAon bariis(tenkan + kijun) / 2as it stooddisplacementbars earlier, andsenkouBis the 52-bar midpoint fromdisplacementbars earlier; on the firstdisplacementbars 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 nodisplacement_bars. The projection past the newest loaded bar is not emitted, exactly as in the engine. chikouis the current close. The engine reads the close of bari + displacement, a future bar, and only falls back to the current close on the lastdisplacementbars of the series; a class that sees one bar at a time cannot read ahead, so the field is the current close. Declaringdisplacement_bars: -26on its output draws that close 26 bars back, which is the lagging span as the engine's chart shows it.om chart indicator previewrefuses 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 shiftedThe 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 200What changed from kScript
vwap(anchor="week")isnew Vwap("week")fed the bar's open time in milliseconds; the anchor is a bucket id from thetimesource, 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 withdisplacement_bars: -26on its output, and the cloud is two gated one-bar boxes. - Supertrend's
.line/.directionstreams are the fields of one object, and the direction-colored line is acolor_byladder over a data-only output instead of acolorIndexexpression. - PSAR reads
high,lowandcloseas declared inputs; nothing reads the chart's OHLC implicitly.