Series functions answer the questions that come up constantly when
writing an indicator: did two lines just cross, is a series rising, what
is the highest high in the last 10 bars, how many bars since a condition
was true, and is this value real or missing. In kScript (legacy) each was
a builtin over a series with history. An Indicator sees one bar at a time
(Execution model), so each is a
small class in src/sdk/ta.ts that keeps the history it needs and folds
one bar per update() call, or a two-line function over NaN. Every
class on this page ships in the kit and matches the kScript engine bar
for bar; the full catalog is on the TA library page.
Price-source helpers
hl2, hlc3, ohlc4, and hlcc4 are arithmetic on inputs. Declare the
fields you read and write the formula:
| Helper | Formula |
|---|---|
hl2 | (high + low) / 2.0 |
hlc3 | (high + low + close) / 3.0 |
ohlc4 | (open + high + low + close) / 4.0 |
hlcc4 | (high + low + close + close) / 4.0 |
There is no implicit source: kScript's hl2() with the source omitted
read the chart's OHLC, while an Indicator names every field as an
input(...) and reads it in state().
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_hl2, out_hlc3, out_hlcc4, out_ohlc4 } from "./gen/outputs";
input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
output("hl2", line, overlay, { color: "#2563eb", width: 1, description: "Bar midpoint" });
output("hlc3", line, overlay, { color: "#16a34a", width: 1, description: "Typical price" });
output("ohlc4", line, overlay, { color: "#f97316", width: 1, description: "Four-price average" });
output("hlcc4", line, overlay, { color: "#7c3aed", width: 1, description: "Close-weighted average" });
let hl2: f64 = NaN;
let hlc3: f64 = NaN;
let ohlc4: f64 = NaN;
let hlcc4: 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();
hl2 = (high + low) / 2.0;
hlc3 = (high + low + close) / 3.0;
ohlc4 = (open + high + low + close) / 4.0;
hlcc4 = (high + low + close + close) / 4.0;
return 1;
}
export function finalize(): void {
out_hl2(hl2);
out_hlc3(hlc3);
out_ohlc4(ohlc4);
out_hlcc4(hlcc4);
emitRow();
}
export function reset(): void {
hl2 = NaN;
hlc3 = NaN;
ohlc4 = NaN;
hlcc4 = NaN;
}Crossovers and signals
Cross ships in src/sdk/ta.ts: new Cross(), .update(a, b): i32
returns +1 on the bar a crosses above b, -1 on the bar it crosses
below, and 0 otherwise, including the first bar and any bar where either
side is NaN. The edge rule is the engine's: a crossover is a below b
on the previous bar and a at or above b on this bar (touching counts
on the current bar, not on the previous one), and a crossunder is the
mirror image. One object replaces the three kScript builtins:
| kScript | Indicator |
|---|---|
crossover(a, b) | cross.update(a, b) == 1 |
crossunder(a, b) | cross.update(a, b) == -1 |
cross(a, b) | cross.update(a, b) != 0 |
Call update() exactly once per bar per pair: it remembers the previous
pair, so a second call on the same bar would compare the bar to itself.
Keep the result in a local and test it as many times as you like. A
crossover against a constant (rsi over 70) is cross.update(value, 70.0).
The classic signal-line pattern is a Cross over the MACD line and its
signal; the result is a 0/1 step you can draw, or a gate for a
shape output that marks the price bar (Plotting):
const crossed = cross.update(macd.macd, macd.signal);
bullish = crossed == 1 ? 1.0 : 0.0;Trend, extremes, and counting
| kScript | Indicator form | Returns |
|---|---|---|
rising(source, period) | new Rising(period), .update(x) | 1 when x is strictly above every one of the previous period values, else 0; NaN for the first period bars |
falling(source, period) | new Falling(period), .update(x) | 1 when x is strictly below every one of the previous period values, else 0; NaN for the first period bars |
change(source, n) | new Change(n), .update(x) | x now minus x n bars ago (n defaults to 1); NaN for the first n bars |
highest(source, period) | new Highest(period), .update(x), field bars | the highest value in the window; bars is the highestbars offset for the same window |
lowest(source, period) | new Lowest(period), .update(x), field bars | the lowest value in the window; bars is the lowestbars offset |
highestbars(source, period) | new HighestBars(period), .update(x), field value | how many bars ago the window's high sits, as 0 or a negative number (0 = this bar, -3 = three bars ago); value is the high itself |
lowestbars(source, period) | new LowestBars(period), .update(x), field value | the same offset for the window's low |
barssince(condition) | new BarsSince(), .update(cond) | bars elapsed since cond was last true (0 on that bar), NaN until it has been true once |
valuewhen(condition, source, occurrence) | new ValueWhen(occurrence), .update(cond, x) | x on the occurrence-th most recent bar where cond was true (0 = the latest, the current bar counts) |
percentile(source, period, pct) | new Percentile(period, pct), .update(x) | the nearest-rank pct-th percentile over the window |
median(source, period) | new Median(period), .update(x) | the middle value (the mean of the two middle values on an even window) |
correlation(s1, s2, period) | new Correlation(period), .update(a, b) | the rolling Pearson correlation, -1..1 |
A condition is an f64: true means finite and not 0, so a 0/1
series or a comparison cast with ? 1.0 : 0.0 both work. kScript's
highest(source=trade, period=20, priceIndex=2) picked the high column
with an index; an Indicator feeds the class the series it should scan,
so new Highest(20) over in_high() is the 20-bar high and a Lowest(20)
over in_low() is the 20-bar low. Among equal values the newest bar wins
the offset. The bars fields are handy for "is the high in the window
recent?" logic, and BarsSince counts up from the last time a condition
fired, the natural cooldown and recency check.
Construct the windowed ones in init() from a param and update each once
per bar:
import { Highest, Lowest, Median, Percentile, Rising } from "./sdk/ta";
let highs = new Highest(20);
let lows = new Lowest(20);
let p80 = new Percentile(20, 80.0);
let median = new Median(20);
const rising = new Rising(3);
// In state(): the return value is the extreme, the field is its offset.
// const hi = highs.update(high); // highs.bars: 0 = this bar, -3 = three bars ago
// const lo = lows.update(low);
// const up = rising.update(close); // 1, 0, or NaN for the first 3 barsEvery windowed class here follows the strict rule of the kScript engine:
NaN until period bars exist, and NaN whenever any value inside the
window is not finite. Percentile sorts the window and takes rank
ceil(pct / 100 * period) (1-based, pct clamped to 0..100); Median
sorts and takes the middle, averaging the two middle values on an even
period; Correlation returns NaN when either side has no variance. None
of them allocates after construction.
Pivot confirmation
pivothigh and pivotlow are causal confirmation signals. A pivot only
emits after rightbars later bars have closed, so the emitted value
appears on the confirmation bar and lags the true pivot bar by
rightbars. Nothing reads a future bar and no earlier bar repaints; an
Indicator could not do otherwise, because state() sees one bar.
new PivotHigh(leftbars, rightbars) and new PivotLow(leftbars, rightbars) keep left + right + 1 bars and, once the window is full,
test the candidate rightbars back: strictly higher than every other
value in the window is a pivot high, strictly lower a pivot low (a tie
never counts), and update(x) returns the candidate's value on the
confirmation bar only and NaN everywhere else, including any window
with a non-finite value. Feed PivotHigh the high and PivotLow the
low, the columns kScript read by default.
import { Fixnan, PivotHigh, PivotLow } from "./sdk/ta";
const pivotHigh = new PivotHigh(2, 2);
const pivotLow = new PivotLow(2, 2);
const lastHigh = new Fixnan();
// In state(): a value on the confirmation bar, NaN between.
// const confirmed = pivotHigh.update(in_high());
// const level = lastHigh.update(confirmed); // the kScript fixnan(pivothigh) idiomA confirmed pivot is a sparse series: one value on the confirmation bar,
NaN between. To carry it forward into a continuous line (the kScript
fixnan(pivotHigh) idiom), run it through Fixnan below.
Handling missing values
Warm-up is NaN, and any arithmetic touching NaN stays NaN, exactly
as na propagated. The helpers are two lines each:
| kScript | Indicator |
|---|---|
isna(x), isnan(x) | isNaN(x) |
isnum(x) | isFinite(x) (finite and not NaN) |
nz(x, replacement) | nz(x, replacement) below |
fixnan(source) | new Fixnan(), .update(x): carries the last finite value forward, NaN until the first one |
import { Fixnan } from "./sdk/ta";
function nz(x: f64, replacement: f64): f64 {
return isNaN(x) ? replacement : x;
}
const fixnan = new Fixnan();
// In state(): fixnan.update(sparse) repeats the last finite value across every NaN bar.Every series function in one module
A sparse series (forced NaN for the first 10 bars) drives the missing
value helpers so you can watch them flip; the rest run over real bars. The
classes are the shipped ones, imported from ./sdk/ta.
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import {
emitRow,
out_barssince,
out_change,
out_correlation,
out_cross,
out_crossover,
out_crossunder,
out_falling,
out_fixnan,
out_highest,
out_highestbars,
out_isna,
out_isnum,
out_lowest,
out_lowestbars,
out_median,
out_nz,
out_percentile,
out_pivot_high,
out_pivot_low,
out_rising,
out_valuewhen,
} from "./gen/outputs";
import { p_fast, p_slow, p_window } from "./gen/params";
import {
BarsSince,
Change,
Correlation,
Cross,
Falling,
Fixnan,
Highest,
Lowest,
Median,
Percentile,
PivotHigh,
PivotLow,
Rising,
Sma,
ValueWhen,
} from "./sdk/ta";
param("fast", 5, { min: 1, max: 200 });
param("slow", 13, { min: 2, max: 400 });
param("window", 10, { min: 2, max: 200, description: "Window for the extremes, percentile, median, and correlation" });
input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
output("crossover", line, lower, { color: "#2563eb", description: "1 on the bar the fast average crosses above the slow" });
output("crossunder", line, lower, { color: "#dc2626", description: "1 on the bar it crosses below" });
output("cross", line, lower, { color: "#7c3aed", description: "1 on either cross" });
output("rising", line, lower, { color: "#16a34a", description: "1 while the close is above its previous 3 values" });
output("falling", line, lower, { color: "#ea580c", description: "1 while the close is below its previous 3 values" });
output("barssince", line, lower, { color: "#0891b2", description: "Bars since the close was above the fast average" });
output("change", line, lower, { color: "#4b5563", description: "3-bar change" });
output("highest", line, lower, { color: "#0f766e", description: "Window highest high" });
output("lowest", line, lower, { color: "#be123c", description: "Window lowest low" });
output("highestbars", line, lower, { color: "#9333ea", description: "Offset of the window high (0 = this bar, negative = bars ago)" });
output("lowestbars", line, lower, { color: "#1d4ed8", description: "Offset of the window low" });
output("valuewhen", line, lower, { color: "#0e7490", description: "The close on the most recent bullish cross" });
output("percentile", line, lower, { color: "#b45309", description: "80th percentile of the close over the window" });
output("median", line, lower, { color: "#a16207", description: "Median close over the window" });
output("correlation", line, lower, { color: "#15803d", description: "Correlation between open and close over the window" });
output("nz", line, lower, { color: "#6d28d9", description: "nz over the sparse series, the open as the replacement" });
output("isna", line, lower, { color: "#0e7490", description: "1 while the sparse series is NaN" });
output("isnum", line, lower, { color: "#374151", description: "1 while the sparse series is a finite number" });
output("fixnan", line, lower, { color: "#3b82f6", description: "The sparse series with gaps carried forward" });
output("pivot_high", line, lower, { color: "#2563eb", description: "Confirmed 2/2 pivot highs, carried forward" });
output("pivot_low", line, lower, { color: "#dc2626", description: "Confirmed 2/2 pivot lows, carried forward" });
function nz(x: f64, replacement: f64): f64 {
return isNaN(x) ? replacement : x;
}
let fast = new Sma(5);
let slow = new Sma(13);
const cross = new Cross();
const rising = new Rising(3);
const falling = new Falling(3);
const barsSince = new BarsSince();
const change = new Change(3);
let highs = new Highest(10);
let lows = new Lowest(10);
const valueWhen = new ValueWhen(0);
let percentile = new Percentile(10, 80.0);
let median = new Median(10);
let correlation = new Correlation(10);
const fixnan = new Fixnan();
const pivotHigh = new PivotHigh(2, 2);
const pivotLow = new PivotLow(2, 2);
const stableHigh = new Fixnan();
const stableLow = new Fixnan();
let barIndex: i32 = 0;
let crossed: i32 = 0;
let risingValue: f64 = NaN;
let fallingValue: f64 = NaN;
let since: f64 = NaN;
let changeValue: f64 = NaN;
let highValue: f64 = NaN;
let lowValue: f64 = NaN;
let whenValue: f64 = NaN;
let pctValue: f64 = NaN;
let medianValue: f64 = NaN;
let corrValue: f64 = NaN;
let sparse: f64 = NaN;
let fixed: f64 = NaN;
let openValue: f64 = NaN;
let pivotHighValue: f64 = NaN;
let pivotLowValue: f64 = NaN;
export function init(): void {
fast = new Sma(i32(p_fast()));
slow = new Sma(i32(p_slow()));
const window = i32(p_window());
highs = new Highest(window);
lows = new Lowest(window);
percentile = new Percentile(window, 80.0);
median = new Median(window);
correlation = new Correlation(window);
}
export function state(): i32 {
const close = in_close();
openValue = in_open();
const f = fast.update(close);
const s = slow.update(close);
crossed = cross.update(f, s);
risingValue = rising.update(close);
fallingValue = falling.update(close);
since = barsSince.update(!isNaN(f) && close > f ? 1.0 : 0.0);
changeValue = change.update(close);
highValue = highs.update(in_high());
lowValue = lows.update(in_low());
whenValue = valueWhen.update(crossed == 1 ? 1.0 : 0.0, close);
pctValue = percentile.update(close);
medianValue = median.update(close);
corrValue = correlation.update(openValue, close);
sparse = barIndex < 10 ? NaN : close;
fixed = fixnan.update(sparse);
pivotHighValue = stableHigh.update(pivotHigh.update(in_high()));
pivotLowValue = stableLow.update(pivotLow.update(in_low()));
barIndex += 1;
return 1;
}
export function finalize(): void {
out_crossover(crossed == 1 ? 1.0 : 0.0);
out_crossunder(crossed == -1 ? 1.0 : 0.0);
out_cross(crossed != 0 ? 1.0 : 0.0);
out_rising(risingValue);
out_falling(fallingValue);
out_barssince(since);
out_change(changeValue);
out_highest(highValue);
out_lowest(lowValue);
out_highestbars(highs.bars);
out_lowestbars(lows.bars);
out_valuewhen(whenValue);
out_percentile(pctValue);
out_median(medianValue);
out_correlation(corrValue);
out_nz(nz(sparse, openValue));
out_isna(isNaN(sparse) ? 1.0 : 0.0);
out_isnum(isFinite(sparse) ? 1.0 : 0.0);
out_fixnan(fixed);
out_pivot_high(pivotHighValue);
out_pivot_low(pivotLowValue);
emitRow();
}
export function reset(): void {
fast.reset();
slow.reset();
cross.reset();
rising.reset();
falling.reset();
barsSince.reset();
change.reset();
highs.reset();
lows.reset();
valueWhen.reset();
percentile.reset();
median.reset();
correlation.reset();
fixnan.reset();
pivotHigh.reset();
pivotLow.reset();
stableHigh.reset();
stableLow.reset();
barIndex = 0;
crossed = 0;
risingValue = NaN;
fallingValue = NaN;
since = NaN;
changeValue = NaN;
highValue = NaN;
lowValue = NaN;
whenValue = NaN;
pctValue = NaN;
medianValue = NaN;
corrValue = NaN;
sparse = NaN;
fixed = NaN;
openValue = NaN;
pivotHighValue = NaN;
pivotLowValue = NaN;
}barIndex is state the module counts itself: there is no barIndex
global, and reset() puts it back to 0 because the host replays the
forming bar through a fresh evaluation of the history.
A Donchian breakout
The kScript "20-bar high and low channel" as a Highest over the high
and a Lowest over the low, with a gated mark on the bar that closes
above the channel. The gate is a data-only output, and the shape output
draws only where it is nonzero.
import { input, line, none, ohlcv, output, overlay, param, shape } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_breakout, out_breakout_mark, out_hi, out_lo } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Highest, Lowest } from "./sdk/ta";
param("period", 20, { min: 2, max: 400, description: "Channel lookback" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
output("hi", line, overlay, { color: "#16a34a", width: 1, description: "Upper Donchian band" });
output("lo", line, overlay, { color: "#dc2626", width: 1, description: "Lower Donchian band" });
output("breakout_mark", shape, overlay, { color: "#16a34a", shape_where: "breakout", description: "The close on a breakout bar" });
output("breakout", none, overlay, { description: "1 when the close is above the previous bar's upper band" });
let highs = new Highest(20);
let lows = new Lowest(20);
let hi: f64 = NaN;
let lo: f64 = NaN;
let prevHi: f64 = NaN;
let close: f64 = NaN;
let breakout: f64 = 0.0;
export function init(): void {
highs = new Highest(i32(p_period()));
lows = new Lowest(i32(p_period()));
}
export function state(): i32 {
close = in_close();
// Test against the channel as it stood BEFORE this bar, so the bar cannot break its own high.
breakout = !isNaN(prevHi) && close > prevHi ? 1.0 : 0.0;
hi = highs.update(in_high());
lo = lows.update(in_low());
prevHi = hi;
return isNaN(hi) ? 0 : 1;
}
export function finalize(): void {
out_hi(hi);
out_lo(lo);
out_breakout_mark(close);
out_breakout(breakout);
emitRow();
}
export function reset(): void {
highs.reset();
lows.reset();
hi = NaN;
lo = NaN;
prevHi = NaN;
close = NaN;
breakout = 0.0;
}Warm-up and the missing-value window
Windowed functions cannot produce a value until they have seen enough
bars: a 20-bar Percentile or Correlation returns NaN until 20 bars
exist, just as a 20-bar Sma does. When the underlying series is itself
NaN for a stretch, the warm-up shifts forward by the same amount,
because the windowed classes refuse a window with a NaN inside it.
Fixnan shows the recovery: it produces its first real value the moment
the source has one, then holds it across every gap. Feed the sparse
series from the tour into a 5-bar Percentile and the percentile
output stays NaN through bar 14, not bar 4.