Kodexius Script
Kodexius Script lets you build your own custom indicators and signals in JavaScript. It serves the same purpose as the chart-scripting languages of other charting platforms, but there is no new language to learn: you write real JavaScript against a small, purpose-built API.
Your script reads the chart's OHLCV data, computes series with built-in technical-analysis functions, and draws the results back onto the chart as lines, histograms, markers, and annotations. Scripts run in a secure, isolated sandbox in your browser, so they never block the interface and cannot access the network or the page.
Table of contents
- Overview
- Quick start
- Execution model
- API reference
- Drawing objects
- Multi-timeframe & multi-symbol data
- Strategies
- Backtesting
- Alerts
- Screener
- AI assistant
- Sharing scripts
- Security and sandbox limits
- Example scripts
- What's next
Overview
A Kodexius Script can:
- Read the chart's OHLCV data through the
open,high,low,close,volume, andtimeseries. - Compute series with built-in functions such as
sma,ema,rsi,atr, andcrossover. - Draw results on the chart:
plot()for lines and histograms,signal()for bar markers. - Add free-form drawings:
line(),label(),box(), a corner-anchoredtable()info panel, multi-pointpolyline()paths and filled polygons, and avolumeProfile()histogram. - Expose user-editable parameters with
input.number(),input.bool(), andinput.select(). - Define alert conditions with
alertcondition(). - Simulate a trading strategy with
strategy(): entries, exits, an equity curve, and performance statistics on the chart.
A minimal example, a 9/21 moving-average cross:
indicator("SMA Cross", { overlay: true });
const fast = sma(close, input.number("Fast", 9));
const slow = sma(close, input.number("Slow", 21));
plot(fast, { title: "Fast SMA", color: "#22d3ee" });
plot(slow, { title: "Slow SMA", color: "#f59e0b" });
signal(crossover(fast, slow), { shape: "arrowUp", color: "#34d399", text: "BUY" });
signal(crossunder(fast, slow), { shape: "arrowDown", color: "#f87171", text: "SELL" });
Quick start
1. Open the editor. On the Supercharts page (/supercharts), click the Script button in the toolbar. A code editor panel opens at the bottom, preloaded with a sample script.
2. Run it. Tick the "Show on chart" checkbox at the top of the panel. Your script runs against the chart's loaded candles and its output is drawn immediately. It re-runs automatically whenever you change the code or an input.
3. Write your first script. Replace the editor contents with:
indicator("My First Indicator", { overlay: true });
const length = input.number("Length", 20, { min: 1, max: 200 });
const average = sma(close, length);
plot(average, { title: "SMA", color: "#22d3ee" });
You will see a moving-average line over the price. Change the "Length" value in the Inputs section of the panel and the line updates instantly.
4. Add a signal.
indicator("Cross", { overlay: true });
const fast = ema(close, 12);
const slow = ema(close, 26);
plot(fast, { title: "EMA 12", color: "#22d3ee" });
plot(slow, { title: "EMA 26", color: "#f59e0b" });
signal(crossover(fast, slow), { shape: "arrowUp", position: "belowBar", color: "#34d399", text: "BUY" });
signal(crossunder(fast, slow), { shape: "arrowDown", position: "aboveBar", color: "#f87171", text: "SELL" });
5. Draw in a separate pane. Pass overlay: false to place a plot below the price in its own pane:
indicator("RSI", { overlay: false });
plot(rsi(close, 14), { title: "RSI", color: "#a78bfa" });
alertcondition(crossunder(rsi(close, 14), 30), "RSI dropped below 30");
Tips
- The editor provides autocomplete and inline type checking for every built-in; type
sma(to see its signature. - Use the Load example menu to try ready-made scripts.
- Errors appear in the side panel with a line number.
console.log(...)output is shown in the side panel for debugging.
Execution model
Series
All market data and indicator output is a series: a plain JavaScript array of numbers (number[]), one value per bar.
close // array of every close price
close.length // number of loaded bars
Indexing is chronological: close[0] is the oldest bar and close[close.length - 1] is the newest.
const lastClose = close[close.length - 1]; // newest bar
const prevClose = close[close.length - 2]; // one bar earlier
The last(series, offset) helper is a convenient shortcut:
last(close) // newest value (close[length - 1])
last(close, 1) // one bar earlier
Vectorized calculation
Built-in functions are vectorized: each one takes the whole series and returns a whole series in a single call.
const avg = sma(close, 20); // one call → an SMA value for every bar
This is natural for JavaScript developers and fast, since each indicator is computed in a single pass. Bars in the warm-up period contain NaN:
sma([1, 2, 3, 4, 5], 3); // [NaN, NaN, 2, 3, 4]
NaN inputs are ignored by the smoothing and windowed functions (sma, ema, rma, stdev, highest, lowest, …), so composing indicators works as expected: sma(rsi(close, 14), 3) simply starts a little later instead of returning an empty series.
plot() automatically skips NaN values, and crossover/crossunder never report a cross involving NaN. Use nz(value, replacement) to replace NaN or Infinity with a fixed value.
When your script runs
Your script re-runs in the sandbox when:
- you edit the code,
- you change an input,
- the symbol or timeframe changes and new candles load,
- a new bar closes (on live data).
Your script runs once per closed bar, not on every intrabar price tick. This keeps performance predictable and matches the semantics used for server-side alerts.
A typical script has four parts:
// 1) Metadata: title and default pane
indicator("Title", { overlay: true });
// 2) Inputs: user-editable parameters
const len = input.number("Length", 14);
// 3) Calculation: built-ins + plain JavaScript
const r = rsi(close, len);
// 4) Output: plots, signals, alerts
plot(r, { title: "RSI", overlay: false });
alertcondition(crossover(r, 70), "Overbought");
indicator() is optional; if omitted, the title defaults to "Kodexius Script" and overlay to true.
API reference
Every built-in takes a Series (number[]) and returns a Series of the same length, with NaN during the warm-up period. NaN values inside the input are ignored: a leading NaN prefix from an upstream indicator only delays the first output, and a window containing an interior NaN yields NaN until the window is clean again.
Length-like parameters (len, pivot bars, offsets) are floored to integers: sma(close, 2.5) behaves exactly like sma(close, 2). A length below the function's minimum (or a non-finite one) deterministically returns an all-NaN series.
Series access
The chart's data is exposed as index-aligned global series (index 0 = oldest bar):
| Series | Description |
|---|---|
open | Open prices |
high | High prices |
low | Low prices |
close | Close prices |
volume | Volume (see Known limitations) |
time | Bar open time, in Unix seconds |
Moving averages
sma(src, len)
Simple moving average: the arithmetic mean of the last len bars.
plot(sma(close, 20), { title: "SMA 20" });
// sma([1,2,3,4,5], 3) → [NaN, NaN, 2, 3, 4]
ema(src, len)
Exponential moving average, with alpha = 2 / (len + 1), seeded with the SMA of the first len bars (first output at index len - 1).
plot(ema(close, 12), { title: "EMA 12" });
rma(src, len)
Wilder's smoothing (RMA / SMMA), with alpha = 1 / len. This is the basis of RSI and ATR.
const wilder = rma(close, 14);
Oscillators
rsi(src, len)
Wilder's Relative Strength Index (0–100). First output at index len.
plot(rsi(close, 14), { title: "RSI", overlay: false });
// Returns 100 on a monotonic rise and 0 on a monotonic fall.
// A window with no movement at all (flat series) returns the neutral 50.
atr(len)
Average True Range. Uses the chart's high/low/close series, so only len is passed.
plot(atr(14), { title: "ATR", overlay: false, type: "histogram" });
Windowed statistics
highest(src, len) / lowest(src, len)
Highest / lowest value over the last len bars. NaN values in the window are ignored; a window that is entirely NaN yields NaN.
const donchianUpper = highest(high, 20);
const donchianLower = lowest(low, 20);
// highest([1,3,2,5,4], 3) → [NaN, NaN, 3, 5, 5]
stdev(src, len)
Population standard deviation over a rolling window.
const deviation = stdev(close, 20);
change(src, offset?)
Difference versus offset bars ago (default offset = 1).
const momentum = change(close, 1);
// change([10,12,9], 1) → [NaN, 2, -3]
Crosses
crossover(a, b) / crossunder(a, b)
Return a boolean[]. crossover is true on the bar where a crosses above b; crossunder on a downward cross. b may be a series or a constant number.
crossover(ema(close, 9), ema(close, 21)); // series vs series
crossover(rsi(close, 14), 70); // series vs constant
// crossover([1,2,3],[3,2,1]) → [false, false, true]
Helpers
last(series, offset?)
Read the value offset bars back from the end (0 = last bar).
const lastRsi = last(rsi(close, 14));
nz(value, replacement?)
Replace NaN / Infinity with replacement (default 0).
const safe = nz(last(atr(14)), 0);
Extended ta library
Higher-level technical-analysis functions, all pure and vectorized (NaN marks warmup). Functions that need volume or the full high/low/close read them from the chart automatically, so you pass only the parameters (like atr(len)). Tuple returns are plain arrays; destructure them.
Derived-price series hl2 ((high+low)/2), hlc3 ((high+low+close)/3) and ohlc4 are available as globals.
| Function | Returns | Notes |
|---|---|---|
wma(src, len) | series | Weighted MA (linear weights). |
hma(src, len) | series | Hull MA (low lag). |
alma(src, len, offset?, sigma?, useFloor?) | series | Arnaud Legoux MA (Gaussian kernel; defaults offset 0.85, sigma 6). |
swma(src) | series | Symmetric weighted MA, fixed length 4 (weights 1/6, 2/6, 2/6, 1/6). |
vwma(src, len) | series | Volume-weighted MA: sma(src*volume, len) / sma(volume, len). |
roc(src, len) | series | Rate of change, percent. |
mom(src, len) | series | Momentum (src - src[len]). |
cmo(src, len) | series | Chande Momentum Oscillator (−100…100; sums of 1-bar gains/losses). |
cog(src, len) | series | Center of Gravity (negative weighted position of the window). |
tsi(src, shortLen, longLen) | series | True Strength Index in [−1, 1] (double-smoothed momentum ratio; multiply by 100 for the classic display). |
rci(src, len) | series | Rank Correlation Index (−100…100; value ranks vs. time order; ties share average ranks). |
macd(src, fast, slow, signal) | [macd, signal, hist] | macd = ema(fast) - ema(slow). |
bb(src, len, mult) | [basis, upper, lower] | Bollinger Bands (basis = sma). |
bbw(src, len, mult) | series | Bollinger Bands Width: (upper - lower) / basis. |
kc(src, len, mult, useTrueRange?) | [middle, upper, lower] | Keltner Channels: middle = ema(src), band span = ema(true range) * mult (high-low span when useTrueRange is false). |
kcw(src, len, mult, useTrueRange?) | series | Keltner Channels Width: (upper - lower) / middle. |
stoch(source, len) | series | Stochastic %K over the chart high/low. |
cci(src, len) | series | Commodity Channel Index (src usually hlc3). |
wpr(len) | series | Williams %R (−100…0) over the chart high/low/close. |
linreg(src, len, offset?) | series | Linear-regression value of the fit. |
correlation(a, b, len) | series | Pearson correlation (−1…1). |
tr(handleNa?) | series | True range. handleNa (default true) controls bar 0, where no previous close exists: true → high - low, false → NaN. |
vwap(source?, anchor?, stdevMult?) | series or [vwap, upper, lower] | See the VWAP note below. |
mfi(source, len) | series | Money Flow Index (source usually hlc3). 100 on a pure rise, 0 on a pure fall; a window with no flow in either direction (flat prices or zero volume) returns the neutral 50, matching rsi's convention. |
obv() | series | On-Balance Volume. |
accdist() | series | Accumulation/Distribution line (cumulative close-location-value volume). |
iii() | series | Intraday Intensity Index, per bar. |
nvi() / pvi() | series | Negative/Positive Volume Index (start at 1; accrue close changes on volume-decrease / volume-increase bars). |
pvt() | series | Price-Volume Trend (cumulative percent-change-weighted volume). |
wad() | series | Williams Accumulation/Distribution (price-only true-range accumulation). |
wvad() | series | Williams Variable A/D, per bar: (close - open) / (high - low) * volume. |
dmi(diLen, adxLen) | [+DI, -DI, adx] | Directional Movement Index. |
adx(diLen, adxLen) | series | Average Directional Index (trend strength). |
supertrend(factor, atrPeriod) | [line, direction] | direction 1 = down, −1 = up. |
sar(start, inc, max) / psar(...) | series | Parabolic SAR (Wilder). |
pivothigh(src, left, right) / pivotlow(...) | series | Pivot value confirmed right bars later. Ties are allowed on the left and strict on the right, so a flat top/bottom (plateau) marks its last bar. The 2-argument form pivothigh(left, right) / pivotlow(left, right) reads the chart high / low. |
pivot_point_levels(type, anchor, developing?) | 11 series | Period pivot ladders; see below. |
barssince(condition) | series | Bars since the condition was last true. |
valuewhen(condition, src, occurrence?) | series | src at the Nth-back true bar. |
cross(a, b) | boolean[] | True where a crosses b in either direction. |
falling(src, len) / rising(src, len) | boolean[] | True when the current value is strictly lower / higher than every one of the previous len values (false during warmup or on NaN). |
highestbars(src, len) / lowestbars(src, len) | series | Offset to the window's extreme: 0 = current bar, -3 = three bars ago. Ties pick the most recent bar. |
cum(src) | series | Running cumulative sum (NaN inputs contribute nothing). |
max(src) / min(src) | series | Running all-time maximum / minimum so far. |
dev(src, len) | series | Mean absolute deviation from the window SMA. |
median(src, len) | series | Rolling median (mean of the two middle values when the window is even). |
mode(src, len) | series | Rolling mode; ties (or no repeated value) resolve to the smallest. |
variance(src, len, biased?) | series | Rolling variance: population by default, sample with biased: false. |
range(src, len) | series | highest - lowest over the window. |
percentile_linear_interpolation(src, len, p) | series | Rolling p-th percentile (0–100), interpolating between the two nearest ranks. |
percentile_nearest_rank(src, len, p) | series | Rolling p-th percentile with the nearest-rank method. |
percentrank(src, len) | series | Percent of the previous len values less than or equal to the current value. |
stdev(src, len, biased?) also gained the biased switch: population deviation by default (unchanged), sample deviation with biased: false.
indicator("MACD + Supertrend", { overlay: false });
const [macdLine, signalLine, hist] = macd(close, 12, 26, 9);
plot(macdLine, { title: "MACD", color: "#22d3ee" });
plot(signalLine, { title: "Signal", color: "#f59e0b" });
plot(hist, { title: "Hist", type: "histogram", color: "#94a3b8" });
const [stLine, stDir] = supertrend(3, 10);
signal(crossover(stDir.map((d) => -d), 0), { position: "belowBar", shape: "arrowUp", color: "#34d399", text: "TREND UP" });
VWAP note. By default
vwap(source?)computes a running VWAP accumulated from the first loaded bar (unchanged behavior). The optionalanchorresets the accumulation at period starts: pass a boolean series (truestarts a new period) or one of the calendar keywords"day","week","month"(UTC boundaries derived from thetimeseries, which is in seconds; weeks start on Monday). A numeric third argument switches the return to[vwap, upper, lower], where the bands sitstdevMultvolume-weighted standard deviations around the VWAP. Bars whose source value isNaNare skipped entirely (neither their price nor their volume enters the accumulation).
pivot_point_levels(type, anchor, developing?)
Anchor-driven period pivot ladders. type is one of "Traditional", "Fibonacci", "Woodie", "Classic", "DM", "Camarilla"; anchor marks period starts (a boolean series, or "day" / "week" / "month"). Returns an array of 11 series [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]; levels a type does not define stay NaN (for example DM only defines P/R1/S1). With developing: false (the default) each bar carries the levels computed from the previous completed period; with developing: true the levels recompute over the current period so far. developing: true is rejected for "Woodie", whose pivot depends on the period open and is therefore never "developing".
indicator("Daily pivots", { overlay: true });
const levels = pivot_point_levels("Traditional", "day");
plot(levels[0], { title: "P", color: "#f59e0b" });
plot(levels[1], { title: "R1", color: "#f87171" });
plot(levels[2], { title: "S1", color: "#34d399" });
Colors
Colors are plain strings the chart consumes directly (#rrggbb, #rrggbbaa or rgba(...)). The color namespace provides the standard named palette (color.red, color.teal, … 17 names) and helpers. Transparency is 0–100 (0 = opaque).
| Function | Description |
|---|---|
color.new(base, transp) | base with its transparency replaced. |
color.rgb(r, g, b, transp?) | Build from 0–255 channels. |
color.from_gradient(value, bottomValue, topValue, bottomColor, topColor) | Linear per-channel blend, clamped at the ends. |
color.r(c) / color.g(c) / color.b(c) / color.t(c) | Channel accessors (t = transparency 0–100). |
const heat = close.map((c, i) => color.from_gradient(rsi(close, 14)[i], 30, 70, color.teal, color.red));
plot(close, { title: "Heat", color: heat });
Strings, arrays and matrices
str.*
| Function | Description |
|---|---|
str.format(fmt, ...args) | Message formatting: {0} inserts an argument, {0,number} formats a number, {0,number,STYLE} accepts integer, percent, currency or a #/0 pattern such as #.##, 0.00, #,##0.00, #.##%. Text in single quotes is literal ('' escapes a quote). Rounding is half away from zero. |
str.format_time(sec, format?, timezone?) | Formats a Unix timestamp in seconds (the engine's time convention - not milliseconds). Tokens: yyyy yy MM M dd d HH H hh h mm m ss s a Z; quoted text is literal. Default format yyyy-MM-dd'T'HH:mm:ssZ. timezone accepts "UTC" (default) or fixed offsets like "UTC+3" / "-05:30"; location names are rejected because the sandbox carries no timezone database. |
str.format_price(value, tickSize) | Snaps value to the nearest tick and renders it with the tick's decimal places (tick 0.25 → 2 decimals). There is no global instrument tick in the sandbox, so the tick size is always passed explicitly. |
array.*
Order-independent statistics over plain number[] values (NaN elements are ignored; inputs are never mutated): array.avg, array.median, array.mode, array.stdev(arr, biased?), array.variance(arr, biased?), array.covariance(a, b, biased?), array.percentile_linear_interpolation(arr, p), array.percentile_nearest_rank(arr, p), array.percentrank(arr, index), array.standardize(arr), array.sort_indices(arr, order?), array.binary_search(arr, v), array.binary_search_leftmost(arr, v), array.binary_search_rightmost(arr, v). The binary searches expect an ascending array; leftmost returns the index of the nearest smaller element when the value is absent (−1 below all), rightmost the index of the nearest greater element (length above all).
For maps, plain JavaScript Map objects work in the sandbox as-is - there is no separate map namespace.
matrix.*
A numeric matrix type with the full construction / mutation / algebra / inspection surface. Create with matrix.new(rows, cols, initialValue?) (default fill NaN); all functions take the handle as their first argument. Invalid shapes or indices throw a clear script error.
- Access & shape:
get,set,rows,columns,elements_count,row,col,fill,copy. - Mutation:
add_row,add_col,remove_row,remove_col,swap_rows,swap_columns,reshape,concat,submatrix,reverse(180° rotation in place),sort(rows by a column, stable). - Algebra:
sum,diff(matrix or scalar),mult(matrix × matrix, matrix × scalar, matrix × vector → array),transpose,trace,det,rank,inv,pinv,pow,kron,eigenvalues,eigenvectors. - Element statistics:
avg,max,min,median,mode. - Inspection:
is_zero,is_identity,is_binary,is_symmetric,is_antisymmetric,is_diagonal,is_antidiagonal,is_triangular,is_stochastic(row sums within 1e-9 of 1),is_square.
Numerics: det/inv/rank use elimination with partial pivoting; pinv uses rank factorization (exact for rank-deficient input); eigenvalues/eigenvectors use the Jacobi iteration for symmetric matrices and a shifted QR iteration otherwise, sorted descending. Matrices with complex eigenvalues are rejected with a clear error - results are never fabricated.
const m = matrix.new(2, 2, 0);
matrix.set(m, 0, 0, 2); matrix.set(m, 1, 1, 3);
console.log(matrix.det(m), matrix.eigenvalues(m)); // 6, [3, 2]
Inputs
Inputs are parameters the user can edit from the side panel. Each input.* call returns the resolved value; invalid saved values fall back to the declared default deterministically.
You can also change inputs directly on the chart: hover the indicator's legend and click the gear (or double-click the legend title) to open a settings panel with the same controls, including a reset to the declared defaults.
Presentation options (accepted by every input kind): tooltip (help text on the control), group (section header), inline (controls sharing the same inline key render on one row), confirm and display (recorded on the spec; display: "none" hides the control from the settings UI).
Input kinds
| Function | Value | Notes |
|---|---|---|
input(name, def, options?) | inferred | Generic form: number default → float, boolean → bool, string → string. |
input.number(name, def, options?) | number | options: { min?, max?, step? } (clamped). |
input.int(name, def, options?) | number | Floored to an integer, then clamped. |
input.float(name, def, options?) | number | Same as input.number; distinct label in the UI. |
input.bool(name, def) | boolean | Checkbox. |
input.select(name, def, options) | string | Dropdown; values outside the list fall back to the default. |
input.enum(name, def, options) | string | Typed choice (same contract as select; its own kind in the UI). |
input.string(name, def, options?) | string | Free text; pass options: [...] to restrict to a list. |
input.text_area(name, def) | string | Multi-line text. |
input.color(name, def) | string | Color (hex/rgba); unparseable values fall back to the default. |
input.source(name, def) | series | The user picks a source NAME (open, high, low, close, hl2, hlc3, ohlc4, volume); the script receives the actual series. |
input.symbol(name, def) | string | Market symbol as a plain validated string (no interactive picker). |
input.timeframe(name, def) | string | Timeframe string such as "1h" / "1D". |
input.session(name, def) | string | "HHMM-HHMM" (24h); anything else falls back to the default. |
input.price(name, def, options?) | number | Plain finite number (no chart-interactive selection). |
input.time(name, def, options?) | number | Unix timestamp in seconds, floored to an integer. |
const len = input.int("Length", 14, { min: 1, max: 200, group: "Core", tooltip: "Lookback bars" });
const src = input.source("Source", "hlc3");
const tint = input.color("Band tint", "#22d3ee");
plot(sma(src, len), { color: tint });
input.symbol/input.timeframe/input.session/input.price/input.timeare plain validated values: they parameterize the script's own logic. Chart-interactive behaviors (tapping a price on the chart, requesting another symbol's data) are out of scope for the sandbox.
Outputs
indicator(title, options?)
Sets the script's display title and default pane. options: { overlay? }.
indicator("My Indicator", { overlay: true });
plot(series, options?)
Draws a series on the chart.
| Field | Type | Default | Description |
|---|---|---|---|
title | string | "Plot N" | Name shown in the legend |
color | string or string[] | automatic | A single color, or a per-bar color array (non-string entries fall back to the series color) |
type | see below | "line" | Render style |
overlay | boolean | indicator() value | true: price pane, false: separate pane |
lineWidth | number | 2 | Line thickness |
style | "solid" | "dashed" | "dotted" | "solid" | Dash pattern for line-drawn styles |
offset | number | 0 | Shift the plot by whole bars (positive → right; shifts past the last bar extend the axis using the last bar spacing) |
showLast | number | (all) | Keep only the last N finite values |
trackprice | boolean | false | Show a horizontal price line at the series' last value |
precision | number | adaptive | Fixed decimal places for the legend value |
Render styles (type): "line", "histogram", "columns", "area", "circles", "stepline", plus the break variants "linebr", "areabr", "steplinebr" which break the drawing at NaN bars instead of connecting across them (the default styles skip NaN bars and connect the neighbors).
plot(sma(close, 50), { title: "SMA 50", color: "#f59e0b", lineWidth: 1 });
plot(rsi(close, 14), { title: "RSI", type: "area", overlay: false, precision: 1 });
const trendColor = close.map((c, i) => (c >= open[i] ? color.teal : color.red));
plot(close, { title: "Trend", color: trendColor });
Per-bar colors are natively supported for every plot style (lines, areas, histograms/columns, circle points) as well as for
plotcandle/plotbarbodies and markers.
While a script is active, each chart pane shows a compact legend in its top-left corner: the script title with its input values in parentheses (hover the parentheses to see the input names), and one line per plot with its color, title and current value. Values track the bar under the crosshair and fall back to the latest bar when the pointer leaves the chart.
signal(condition, options?)
Places a chart marker on every bar where the boolean series is true.
| Field | Type | Default |
|---|---|---|
color | string | "#22d3ee" |
position | "aboveBar" | "belowBar" | "inBar" | "aboveBar" |
shape | "circle" | "arrowUp" | "arrowDown" | "square" | "arrowDown" |
text | string | (none) |
signal(crossover(fast, slow), { shape: "arrowUp", position: "belowBar", color: "#34d399", text: "BUY" });
alertcondition(condition, message)
Records an alert on every bar where the condition is true. Turn a saved script into a running alert to have these evaluated on the server every closed bar; see Alerts.
alertcondition(crossunder(close, sma(close, 200)), "Price fell below the 200 SMA");
console.log(...)
Debug output, shown in the side panel (first 200 lines).
console.log("last RSI:", last(rsi(close, 14)));
Visual outputs
Higher-level rendering beyond a single line or marker.
hline(price, options?)
Draws a horizontal level line at a constant price on the price pane (or the oscillator pane with overlay: false). This replaces the old series.map(() => level) idiom. Options: { title?, color?, lineWidth?, style?, overlay? } - style accepts "solid" (default), "dashed" or "dotted".
plot(rsi(close, 14), { title: "RSI", overlay: false });
hline(70, { title: "Overbought", color: "#f87171", style: "dashed", overlay: false });
hline(30, { title: "Oversold", color: "#34d399", style: "dashed", overlay: false });
plotshape(condition, options?) / plotchar(condition, options?)
Place a shape (plotshape) or a glyph/character (plotchar) on every bar where the boolean series is true: like signal, with more variety. plotshape shapes: circle, square, arrowUp/arrowDown, triangleUp/triangleDown, diamond. plotchar renders its char as the marker text.
Shared options: color (single color or per-bar array), size ("tiny" | "small" | "normal" | "large" | "huge"), offset (shift the markers by whole bars) and position. Besides aboveBar/belowBar/inBar, position: "absolute" pins the marker to an exact price: pass price as a single number or a per-bar series (bars whose price is NaN draw nothing).
plotshape(crossover(fast, slow), { shape: "triangleUp", position: "belowBar", color: "#34d399", text: "BUY", size: "large" });
plotchar(rsi(close, 14).map((r) => r > 70), { char: "★", position: "aboveBar", color: "#f59e0b" });
plotshape(pivothigh(2, 2).map(Number.isFinite), { position: "absolute", price: high.map((h) => h * 1.01) });
Marker text color: the chart's marker plugin renders the marker glyph and its text in ONE color, so a separate text color is not supported for
plotshape/plotcharmarkers. Use alabel(which has a realtextColor) when you need independently colored text.
plotarrow(series, options?)
Draws magnitude-scaled arrows from a number series: positive values draw an up arrow below the bar, negative values a down arrow above it, and the arrow size scales linearly with |value| relative to the series' largest |value|. Zero/NaN bars draw nothing. Options: { colorup?, colordown?, offset? } (colors accept per-bar arrays).
const impulse = change(close, 5);
plotarrow(impulse, { colorup: "#34d399", colordown: "#f87171" });
plotcandle(open, high, low, close, options?) / plotbar(...)
Draw an extra candlestick (plotcandle) or OHLC bar (plotbar) series from four value series - for example a smoothed candle model. Options: { title?, color?, wickColor?, borderColor?, overlay? }; color/wickColor/borderColor accept per-bar arrays (plotbar supports color only). Bars with any non-finite component are skipped.
const o = sma(open, 3), h = sma(high, 3), l = sma(low, 3), c = sma(close, 3);
plotcandle(o, h, l, c, { title: "Smoothed", color: c.map((v, i) => (v >= o[i] ? color.teal : color.red)) });
barcolor(colors, options?)
Recolors the main chart candles per bar: pass a color series where each entry is a color string or null/undefined to leave that bar untouched. Options: { offset? }. Applies when the main chart renders candles or bars (line/area chart types have no per-bar body to color).
barcolor(rsi(close, 14).map((r) => (r > 70 ? "#f87171" : r < 30 ? "#34d399" : null)));
fill(seriesA, seriesB, options?)
Shades the band between two plotted series (pass the same series you gave to plot). Options: { color?, opacity?, topColor?, bottomColor? } (opacity 0–1, default 0.12). color accepts a per-bar array, which paints per-segment strips; topColor + bottomColor switch the fill to a vertical gradient across the band.
const [basis, upper, lower] = bb(close, 20, 2);
plot(upper, { title: "Upper" });
plot(lower, { title: "Lower" });
fill(upper, lower, { topColor: "rgba(34, 211, 238, 0.3)", bottomColor: "rgba(34, 211, 238, 0.02)", opacity: 1 });
bgcolor(condition, options?)
Shades the background of bar columns where the boolean series is true. Options: { color?, opacity?, offset? } (default opacity 0.08). color accepts a per-bar array (read at each true bar); offset shifts the shaded columns by whole bars.
bgcolor(rsi(close, 14).map((r) => r > 70), { color: "#f87171" }); // highlight overbought bars
Caps: up to 40 fill and 40 bgcolor layers per run (see Security & limits).
Drawing objects
Drawing functions add free-form objects to the chart: lines, labels, boxes, corner-anchored info panels (tables), multi-point polylines and polygons, and volume profiles. Each takes a single options object.
Coordinates: x is a bar time (Unix seconds) by default; use the time series, e.g. time[time.length - 1] (last bar) or time[0] (oldest bar). Passing xloc: "index" switches the object to bar indices instead: x: 0 is the oldest bar, and indices past the last bar extrapolate deterministically using the last bar spacing (so an object can point into the near future). y is a price. Tables do not use coordinates; they are anchored to a chart corner.
Invalid arguments (such as a non-finite coordinate or a missing label text) and exceeding a limit produce a structured script error, not a crash: the script does not run and a clear message appears in the panel. See Security and sandbox limits for the limits.
Object handles (Line.new, Label.new, Box.new, Table.new, Polyline.new)
Every drawing type has two calling styles that share one implementation and the same per-run caps:
- the one-shot functions (
line(...),label(...),box(...),table(...),polyline(...)) draw and forget; - the object constructors (
Line.new(...),Label.new(...),Box.new(...),Table.new(...),Polyline.new(...)) take the same options object and return a live handle withset_*/get_*accessors,delete()andcopy(). The run serializes the final state of every live object when the script finishes, so you can create an object early and reposition or restyle it later in the script.delete()removes the object (and frees its cap slot);copy()clones it as a new object. Each type keeps a registry of its live handles:Line.all,Label.all,Box.all,Table.all,Polyline.all(oldest first).
| Handle | Setters | Getters |
|---|---|---|
Line | set_xy1(x, y), set_xy2(x, y), set_color, set_width, set_style, set_extend | get_x1/y1/x2/y2, get_color |
Label | set_xy(x, y), set_text, set_color, set_textcolor, set_size, set_position | get_x, get_y, get_text |
Box | set_bounds(x1, y1, x2, y2), set_lefttop(x, y), set_rightbottom(x, y), set_color, set_border_color, set_border_width, set_fill_opacity | get_x1/y1/x2/y2 |
Table | set_cell(row, col, cell) (grows the grid; empty cells pad new space), set_position | get_position |
Polyline | set_points(points), set_color, set_width, set_style | get_points() |
The xloc given at construction applies to every later coordinate setter of that handle.
// A projection line that is revised as the script computes more context.
const trend = Line.new({ x1: 0, y1: close[0], x2: close.length - 1, y2: last(close), xloc: "index", style: "dashed" });
const fit = linreg(close, 50);
trend.set_xy1(close.length - 50, fit[close.length - 50]);
trend.set_xy2(close.length + 5, last(fit)); // extends past the last bar
if (last(rsi(close, 14)) > 70) trend.set_color("#f87171");
console.log("live lines:", Line.all.length);
line
line(args) draws a line segment between two (time, price) points.
| Field | Type | Default | Description |
|---|---|---|---|
x1, y1, x2, y2 | number | (required) | Start and end (time, price) |
color | string | "#22d3ee" | Line color |
width | number | 1 | Thickness |
style | "solid" | "dashed" | "dotted" | "solid" | Line pattern |
extend | "none" | "left" | "right" | "both" | "none" | Extend to the pane edge |
// Mark the last close level with a dashed line extended to the right
const y = last(close);
line({ x1: time[0], y1: y, x2: time[time.length - 1], y2: y, color: "#f59e0b", style: "dashed", extend: "right" });
label
label(args) places a text label anchored at a (time, price) point.
| Field | Type | Default | Description |
|---|---|---|---|
x, y | number | (required) | Anchor point (time, price) |
text | string | (required) | Label text |
color | string | "#1f2937" | Chip background color |
textColor | string | "#e5e7eb" | Text color |
size | "small" | "normal" | "large" | "normal" | Font size |
position | "top" | "bottom" | "left" | "right" | "center" | "top" | Position relative to the point |
label({ x: time[time.length - 1], y: last(high), text: "Peak", position: "top", color: "#7f1d1d", textColor: "#fecaca" });
box
box(args) draws a rectangle between two (time, price) corners.
| Field | Type | Default | Description |
|---|---|---|---|
x1, y1, x2, y2 | number | (required) | Two opposite corners |
color | string | "#22d3ee" | Fill color |
fillOpacity | number (0..1) | 0.1 | Fill transparency |
borderColor | string | color | Border color |
borderWidth | number | 1 | Border thickness (0 = no border) |
box({ x1: time[0], y1: last(highest(high, 50)), x2: time[time.length - 1], y2: last(lowest(low, 50)), fillOpacity: 0.06 });
table
table(args) draws an info panel (a table of text cells) anchored to a chart corner.
| Field | Type | Default | Description |
|---|---|---|---|
position | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-right" | Corner |
rows | Cell[][] | (required) | Rows of cells; each cell is a string or { text, color?, textColor?, align? } |
color | string | "#0a0a0a" | Panel background |
textColor | string | "#e5e7eb" | Default text color |
borderColor | string | "#334155" | Border color |
Cell alignment align: "left" \| "center" \| "right".
table({
position: "top-right",
rows: [
[{ text: "Metric", textColor: "#94a3b8" }, { text: "Value", align: "right", textColor: "#94a3b8" }],
["Last price", { text: last(close).toFixed(2), align: "right" }],
["RSI(14)", { text: last(rsi(close, 14)).toFixed(1), align: "right" }],
],
});
polyline
polyline(args) draws an ordered multi-point path. One function covers three shapes: an open path (default), a closed outline (closed: true links the last point back to the first), and a filled polygon (fill: true, which implies closed). There is no separate polygon() function.
| Field | Type | Default | Description |
|---|---|---|---|
points | { x, y }[] | (required) | At least 2 (time, price) points, in drawing order |
color | string | "#22d3ee" | Stroke color |
width | number | 1 | Stroke thickness |
style | "solid" | "dashed" | "dotted" | "solid" | Stroke pattern |
closed | boolean | false | Connect the last point back to the first |
fill | boolean | false | Shade the enclosed area (implies closed) |
fillColor | string | color | Fill color |
fillOpacity | number (0..1) | 0.1 | Fill transparency |
// A filled triangle under the last three pivot points
polyline({
points: [
{ x: time[time.length - 30], y: last(low, 29) },
{ x: time[time.length - 15], y: last(high, 14) },
{ x: time[time.length - 1], y: last(low) },
],
fill: true, fillColor: "#22d3ee", fillOpacity: 0.08,
});
volumeProfile
volumeProfile(args) computes a volume-by-price histogram from the loaded candles and draws it as horizontal bars hugging one side of the price pane.
The computation is deterministic: the price range [lowest low, highest high] of the selected bars is split into bins equal buckets, and each bar's volume is distributed across the buckets its low..high range overlaps, weighted by overlap. The bucket with the most volume is the point of control (POC); the value area is the smallest set of buckets around the POC that accumulates the valueArea fraction of total volume. POC and value-area buckets are colored distinctly.
| Field | Type | Default | Description |
|---|---|---|---|
bins | number | 24 | Number of price buckets |
from, to | number | whole range | Bar-time range (Unix seconds) to profile |
side | "right" | "left" | "right" | Pane edge the histogram hugs |
widthFraction | number (0..1) | 0.3 | Longest bar as a fraction of the pane width |
valueArea | number (0..1) | 0.68 | Value-area volume fraction (0 disables) |
color | string | "#3b82f6" | Regular bucket color |
valueAreaColor | string | "#22d3ee" | Value-area bucket color |
pocColor | string | "#f59e0b" | Point-of-control color |
// Profile of the most recent 100 bars, anchored right
volumeProfile({ bins: 30, from: time[Math.max(0, time.length - 100)], widthFraction: 0.25 });
Note: the profile is computed from the
volumeseries (real traded volume as reported by the venue). A market whose venue reports no volume renders an empty profile.
Multi-timeframe & multi-symbol data
Scripts can request candles for other intervals and other symbols: for example a daily trend filter on a 1-hour chart, or a reference market's momentum next to the chart's own. The headline property of this API is that it is repaint-safe by default: what a historical bar "saw" is exactly what it would have seen live.
requestHistory(symbol, interval, options?)
indicator("Daily trend on 1h", { overlay: true });
// Daily candles of the CHART's own symbol ("" = chart symbol), aligned to
// the chart timeline: bar i carries the last daily bar CLOSED by bar i's close.
const daily = requestHistory("", "1d");
plot(daily.close, { title: "Prev daily close", type: "stepline" });
// Another market at another interval works the same way.
const btc4h = requestHistory("BINANCE:SPOT:BTCUSDT", "4h");
signal(crossover(close, daily.close), { text: "Above prev daily close" });
symbol: a market symbol or canonical key ("BINANCE:SPOT:BTCUSDT"); the empty string""means the chart's own symbol. Symbols are validated against the market catalog; unknown markets throw a clear error at the call site.interval: one of1m 5m 15m 30m 1h 2h 4h 6h 1d 1w 1M.- Returns
{ time, open, high, low, close, volume }. In the default aligned mode every array has the chart's own length andtimeis the chart's own time array.
Only closed bars are ever served: the platform cuts the forming bar (and, in backtests, anything beyond the evaluated window) before the data reaches your script. The same API works identically in the chart preview, alerts, backtests, and the screener.
Repaint-safety
For a higher interval, the naive approach (giving each chart bar the higher-interval bar it falls inside) leaks the future: a 4h bar spanning 16:00–20:00 only becomes known at 20:00, yet naive alignment would show its final OHLC on the 16:00, 17:00 and 18:00 chart bars. Indicators built that way look brilliant in history and fall apart live.
requestHistory aligns on bar closes instead: chart bar i carries the values of the last requested-interval bar whose close is at or before chart bar i's close (UTC, epoch-second bucket math; calendar-exact for 1M). Concretely, on a 1h chart with a 4h request:
| Chart bar (closes at) | 4h bar it sees |
|---|---|
| 17:00, 18:00, 19:00 | none yet → NaN (or the previous closed 4h bar) |
| 20:00 | the 16:00–20:00 bar, the moment it closes |
| 21:00 … 23:00 | still the 16:00–20:00 bar |
| 00:00 | the 20:00–00:00 bar |
The leading NaN stretch is normal warmup, exactly like sma before its window fills. NaN-aware built-ins compose cleanly with aligned series.
Raw mode and alignTo(chartTime, htfTime, htfValues, htfInterval, chartInterval?)
To compute an indicator on the other interval's own bars (rather than on the chart-aligned step series), request the raw arrays and map the result back yourself:
// RSI computed on native daily bars, then aligned without lookahead.
const d = requestHistory("", "1d", { aligned: false }); // native daily timeline
const dailyRsi = rsi(d.close, 14); // 1 value per daily bar
const rsiOnChart = alignTo(time, d.time, dailyRsi, "1d");
plot(rsiOnChart, { title: "Daily RSI", overlay: false });
alignTo uses the same closed-bar rule as the aligned mode: result entry i is the htfValues entry of the last htfTime bar closed by chart bar i's close, NaN before the first one. htfInterval is required so bar closes are exact (calendar months included). chartInterval is optional; without it the chart bar span is estimated from the smallest positive step of chartTime, which can only delay visibility, never leak it.
Budgets and rules
- Unique requests per run are budgeted (default 5; platform-configurable). Repeating the same
(symbol, interval)pair is free and returns the same data. Exceeding the budget throws. - Bars per request are capped (default 500 newest closed bars). Requesting a lower interval than the chart's own therefore covers only the newest part of a deep chart window; earlier chart bars see
NaN. - Call
requestHistoryunconditionally (top level of your script) with arguments that do not depend on data returned by otherrequestHistorycalls: the platform prefetches every request before the real run, so a request only reachable through another request's data cannot be prefetched and fails with a clear error. - Per-request failures (unknown market, unsupported interval, no data) throw at the call site with the reason; other requests are unaffected.
Strategies
A strategy script simulates trading rules over the loaded chart history and shows the outcome directly on the chart: entry/exit markers on the price pane, an equity curve in its own pane, and a Strategy tab in the script dock with performance statistics and the trade list. The simulation runs in the same sandbox as everything else and is fully deterministic: the same script over the same data always produces the same trades, to the last digit. For longer histories, server-side backtesting runs this exact engine over up to 10,000 bars.
Declaring a strategy
Call strategy(options) once, before declaring orders. This switches the script into strategy mode.
strategy({
title: "SMA Cross Strategy",
initialCapital: 10000, // default 10000
sizing: { type: "percent", value: 95 }, // default { type: "fixed", value: 1 }
commissionBps: 10, // basis points per side, default 0
});
| Field | Type | Default | Description |
|---|---|---|---|
title | string | (none) | Display title (like indicator()) |
initialCapital | number | 10000 | Starting capital |
sizing | { type, value } | { type: "fixed", value: 1 } | Position sizing model (see below) |
commissionBps | number | 0 | Commission in basis points of notional, charged per side |
slippageBps | number | 0 | Slippage in basis points applied to market and stop fills (buys pay more, sells receive less); limit-priced fills are exempt |
pyramiding | number | 0 | Max concurrent same-direction entries (0 → a single position) |
A strategy script can still use every other feature: plot, signal, drawings, inputs, and alertcondition.
Orders
Orders are declared vectorized, like everything else in Kodexius Script: instead of being called once per bar, each order function takes a boolean when series that marks the signal bars.
const fast = sma(close, 9);
const slow = sma(close, 21);
strategy.entry("trend", "long", { when: crossover(fast, slow) });
strategy.close("trend", { when: crossunder(fast, slow) });
strategy.entry(id, direction, { when, qty?, type?, limit?, stop?, comment?, ocaName?, ocaType?, alertMessage? })opens a position ("long"or"short"). Withpyramiding: 0(default) an opposite or differently-tagged position is closed first and the new one opens at the same fill price (reversal); a same-id, same-direction entry while open is ignored. Withpyramiding: Nup toNconcurrent same-direction entries stack.qtyoverrides the sizing model.- Order types via
type:"market"(default, fills at the next bar open);"limit"(needslimit);"stop"(needsstop);"stoplimit"(needs both; the stop arms a resting limit). A limit/stop order placed after a signal rests and fills the first later bar its trigger is reached (see the fill model). - Re-signalling the same
idwhile its limit/stop order is still pending replaces the pending order with the new one ("modify" semantics), so duplicate orders never accumulate. - OCA groups: orders sharing an
ocaNamereact when one member actually fills:ocaType: "cancel"(the default for grouped orders) removes the siblings' pending orders,"reduce"shrinks their quantity by the actually filled amount (reversal/consumption closes plus the opened quantity, which risk caps such asmax_position_sizemay clip below the declaredqty; siblings are removed at 0),"none"leaves them independent. The group also links same-bar market siblings: a member filling anywhere within a bar cancels/reduces the group's remaining market entries of that bar, not just its resting orders. An order that is suppressed entirely - blocked direction with nothing to close, pyramiding cap, same-iddedup, zero permitted quantity - transacts nothing and leaves its group untouched. alertMessagerecords a message on the run's alerts channel at the moment the order fills (and flows into server-side alert delivery like analertconditionmessage).
- Order types via
strategy.order(id, direction, { ... })is the raw order form: same options asstrategy.entry, but no reverse-all semantics - an opposite-direction raw order consumes existing positions by quantity (oldest first; newest first undercloseEntriesRule: "ANY") and any remainder opens a new position. Use it for partial scale-ins/outs.strategy.close(id, { when, comment? })closes the position opened under the sameidat the next bar open.strategy.close_all({ when, comment? })closes every open position regardless ofid.strategy.cancel(id, { when })/strategy.cancel_all({ when })remove pending (unfilled limit/stop) orders wherewhenis true; the cancellation takes effect before the next bar's fills.strategy.exit(id, { when?, stopLoss?, takeProfit?, stopLossOffset?, takeProfitOffset?, lossTicks?, profitTicks?, trailOffset?, trailPrice?, trailActivationOffset?, qty?, qtyPercent?, alertMessage?, comment? })is a real bracket exit:- Stop-loss / take-profit as absolute prices (
stopLoss/takeProfit), distances from entry (…Offset), or tick counts (lossTicks/profitTicks, converted through the strategy config'stickSize; using ticks without atickSizethrows). - A trailing stop (
trailOffsetdistance from the best price). By default it is armed at entry (initial floorentry − trailOffsetfor longs). Adding an activation -trailPrice(absolute level) ortrailActivationOffset(distance from entry) - arms the trail only after the level trades: the anchor starts at the arming bar's favorable extreme, and ratcheting/triggering begin the next bar. An unactivated trail never exits. - Partial exits:
qty(units) orqtyPercent(0–100] close part of the position when the bracket triggers; the triggered rule is consumed and the remainder keeps running unprotected by that rule. - Brackets are checked intrabar, starting on the entry bar itself; you can also pass
whenfor a conditional market close. alertMessagefires only when this exit closes the position - its stop/target/trail triggering, or itswhenclose filling. Closes from other sources (a reversal, a risk-rule liquidation,strategy.close/strategy.close_all) never emit the exit's message.
- Stop-loss / take-profit as absolute prices (
strategy.default_entry_qty(fillPrice)returns the quantity the configured default sizing would produce at a price (percent sizing uses the initial capital, since this helper is a pure function while the live simulation sizes from realized cash at each fill).
strategy.entry("l", "long", { when: buySignal, type: "stop", stop: high[high.length - 1] });
strategy.exit("l", { stopLossOffset: last(atr(14)) * 2, takeProfitOffset: last(atr(14)) * 4, trailOffset: last(atr(14)) * 3 });
The fill model
The fill model is deliberately simple, pessimistic about information, and worth knowing precisely:
- A market signal on bar N fills at the open of bar N + 1. Bar N's values (including its close) are only known once the bar has closed, so the earliest realistic fill is the next bar's open. There is no lookahead: nothing about bar N + 1 influences the signal on bar N.
- A signal on the last loaded bar never fills: there is no next bar yet.
- When several orders act on the same bar, the order is chronological: open-priced market closes first (the open is the bar's first instant), then bracket exits, then working-order fills, then entries, each group in the order the script declared them; finally brackets are checked once more for positions opened on this bar.
- Limit / stop orders rest from the bar after their signal and fill intrabar the first bar their trigger is reached, using only that bar's OHLC (gap-aware): a buy limit fills at
min(open, limit)oncelow ≤ limit; a buy stop atmax(open, stop)oncehigh ≥ stop(mirror for sells). When several resting orders trigger on the same bar, they fill in the order they were placed (declaration order for orders signalled on the same bar) - the true intrabar sequence is unknowable from OHLC, so the placement sequence is the deterministic tiebreak. Re-signalling the sameidreplaces the pending order. A stop-limit arms its limit when the stop triggers: on the trigger bar the effective price ismax(open, stop)(buy; mirror for sells): if the limit permits, it fills right there, otherwise it rests as a plain limit from the next bar on. It can never fill at a price from before its own trigger. - Bracket exits are checked intrabar, including on the entry bar. If one bar could hit both the stop and the target, the stop is assumed to fill first (conservative). A trailing stop without an activation is armed at entry (initial floor
entry ∓ trailOffset), ratchets from each bar's extreme and is tested against the level carried in from the prior bar; withtrailPrice/trailActivationOffsetit arms only once the activation level trades and starts ratcheting the next bar. - Slippage worsens market and stop fills: with
slippageBps, buys payprice × (1 + bps/10000)and sells receiveprice × (1 − bps/10000); withslippageTicks+tickSizethe adjustment is the absolute amountticks × tickSize. Limit-priced fills (limit entries, take-profits and a stop-limit's limit leg) are exempt, because a limit order can never fill worse than its limit price. - A position still open on the last bar stays open: it contributes unrealized profit to the equity curve but is not counted in
totalTradesor the trade list. - With
processOrdersOnClose: truethe whole signal→fill mapping shifts: a signal on bar N fills at bar N's close (still only closed data - the close is known the instant the bar completes), resting orders arm on bar N + 1, and a signal on the last loaded bar fills at that close.
Position sizing and commission
{ type: "fixed", value: q }: every entry trades exactlyqunits.{ type: "percent", value: p }: an entry investsp% of realized equity (initial capital plus the realized profit of all closed trades) at the fill price:qty = equity × p / 100 ÷ fillPrice. Quantities are fractional; nothing is rounded.{ type: "cash", value: c }: an entry invests a fixed currency notional:qty = c ÷ fillPrice.commissionBpschargesnotional × bps / 10000on the entry and the exit (a 10 bps setting costs 0.1% of notional per side). The typedcommissionobject overrides it when present:{ type: "percent", value }(percent of notional per side),{ type: "cash_per_contract", value }(value × qtyper side) or{ type: "cash_per_order", value }(flat per fill). Commission is deducted from each trade'spnl.
Strategy configuration
strategy(options) accepts, in addition to title, initialCapital, sizing, commissionBps/commission, slippageBps and pyramiding:
| Option | Description |
|---|---|
slippageTicks + tickSize | Absolute slippage of slippageTicks × tickSize on market and stop fills (overrides slippageBps; slippageTicks without a positive tickSize throws). tickSize is also required by the tick-based exit offsets - there is no global instrument tick in the sandbox, so it is always explicit. |
marginLong / marginShort | Margin requirement in percent: an entry is trimmed so the same-direction position notional stays within cash × 100 / margin (evaluated against realized cash, deterministically). 0 (default) disables the cap. There is no forced margin-call liquidation. |
processOrdersOnClose | Signals fill at their own bar's close instead of the next bar's open. Resting limit/stop orders arm on the next bar (the signal bar has already traded), entry-bar brackets start on the next bar, and a signal on the last loaded bar does fill (at its close). |
closeEntriesRule | How quantity-consuming exits (raw orders, partial exits) pick entries: "FIFO" (default, oldest first) or "ANY" (newest first). |
riskFreeRate | Annual risk-free rate used by the monthly Sharpe/Sortino statistics (default 0.02). |
calcOnEveryTick and useBarMagnifier are rejected with a clear error: the engine evaluates closed bars of the chart timeframe only, so tick-level recalculation and intrabar magnification cannot be honored (accepting them silently would misrepresent the results). Account-currency conversion is likewise out of scope: the sandbox has no FX rate source, so all strategy amounts are denominated in the instrument's quote currency.
Risk rules (strategy.risk.*)
Deterministic account-protection rules, evaluated on closed data only. A violation cancels all pending orders immediately and blocks new fills - for the rest of the UTC day for the intraday rules, permanently otherwise. Every rule except max_intraday_filled_orders also force-closes open positions at the next bar's open (trade comment "risk"); the order-count rule caps order flow, not exposure, so open positions stay open and their exits keep executing. Configure risk rules before reading any strategy state.
| Function | Effect |
|---|---|
strategy.risk.allow_entry_in(direction) | Only "long" or "short" entries may open positions ("all" removes the filter). A blocked-direction entry still closes opposite positions - it just never reverses into one. |
strategy.risk.max_cons_loss_days(count) | Halt permanently after count consecutive losing days (UTC days; a day loses when its closing equity is below the prior day's). |
strategy.risk.max_drawdown(value, "cash" | "percent") | Halt permanently when the equity drawdown from its running peak reaches value (currency, or percent of the peak). |
strategy.risk.max_intraday_filled_orders(count) | Block new fills for the rest of the day once count orders (entries and exits both count) have filled that day. Open positions are not force-closed: brackets and explicit closes keep executing. |
strategy.risk.max_intraday_loss(value, "cash" | "percent") | Block new fills and close positions for the rest of the day when the day's loss from its starting equity reaches value. The rule stays armed even on a day whose fills max_intraday_filled_orders already blocked: the protective liquidation still fires. |
strategy.risk.max_position_size(contracts) | Cap the total same-direction quantity; entries are trimmed to fit (an entry with no room is skipped). |
Broker state from within the script
The broker simulation is queryable during the script run. Each getter returns the whole per-bar series when called with no argument, or a single value for a bar index. All values are closed-bar states (after that bar's fills, marked at its close).
| Getter | Meaning |
|---|---|
strategy.positionSize(i?) | Signed open quantity (long positive, short negative, 0 flat) |
strategy.positionAvgPrice(i?) | Volume-weighted average entry price (NaN when flat) |
strategy.equity(i?) | Cash + open profit |
strategy.openProfit(i?) | Unrealized profit of the open position |
strategy.netProfit(i?) | Realized net profit |
strategy.openTrades(i?) / strategy.closedTrades(i?) | Open entry count / closed trade count so far |
strategy.winTrades(i?) / strategy.lossTrades(i?) | Winning / losing closed trades so far |
strategy.maxDrawdown(i?) / strategy.maxRunup(i?) | Largest equity drawdown / run-up seen so far |
strategy.closedTradeEntryPrice(k), …ExitPrice(k), …EntryTime(k), …ExitTime(k), …Qty(k), …Profit(k), …Direction(k) | Fields of the k-th closed trade (chronological by exit) |
Determinism guard. Because fills happen after signals, state at bar i depends only on orders signalled before it. Reading state up to bar Q therefore freezes bars ≤ Q: any later strategy.* declaration whose signals would change those bars throws a clear error (the answers already handed out would have been wrong). Declare all orders first, then read state - or only read state for bars earlier than any signal a later declaration produces. The per-trade accessors and full-series getters expose end-of-run information, so they freeze the whole window. Risk-rule and configuration changes after any state read are rejected the same way.
strategy({ initialCapital: 10000 });
strategy.entry("l", "long", { when: crossover(sma(close, 9), sma(close, 21)) });
strategy.close("l", { when: crossunder(sma(close, 9), sma(close, 21)) });
// After declaring all orders, broker state is fully queryable:
plot(strategy.equity(), { title: "Equity", overlay: false });
console.log("open now:", strategy.positionSize(close.length - 1));
Results and statistics
The equity curve is marked to market on every bar close: equity[i] = realized cash + unrealized PnL at close[i]. The Strategy tab reports:
| Statistic | Definition |
|---|---|
| Net profit | final equity − initial capital (includes unrealized PnL of an open position); also as a percent of initial capital |
| Max drawdown | Largest peak-to-trough drop of the equity curve, in currency; also as a percent of the running peak |
| Max run-up | Largest trough-to-peak rise of the equity curve, in currency; also as a percent of the running trough |
| Win rate | Fraction of closed trades with positive pnl (breakeven trades count in the denominator) |
| Profit factor | Gross profit ÷ gross loss (∞ when there are no losing trades) |
| Gross profit / gross loss | Sum of winning trades' profits / losing trades' losses |
| Total trades / win trades / loss trades | Counts over closed trades |
| Final equity | Equity on the last bar |
| Avg win / Avg loss | Mean profit of winning trades / mean loss of losing trades (pnl of exactly 0 counts as neither) |
| Max cons. losses | Longest run of consecutive losing trades (a breakeven trade breaks the run) |
| Sharpe / Sortino | Monthly-return ratios: the equity curve is sampled at the last bar of each UTC month (plus the final bar), differenced into monthly returns, and compared against the monthly risk-free rate (riskFreeRate / 12, default 2% annually). The sample series is seeded with the initial capital at the window's start, so when the window opens mid-month the first return covers only that partial month (and the last sample covers the partial month up to the final bar). Sharpe divides the excess mean by the population standard deviation of the returns; Sortino by the downside deviation versus the same target. 0 when undefined. |
| Trade Sharpe / Trade Sortino | The previous per-trade return ratios (unannualized), kept under distinct names |
Same-bar stop + target: when one bar's range could hit both the stop and the target, the stop is assumed to fill first. This is the conservative deterministic choice - the intrabar path is unknowable from OHLC data.
Backtesting
The chart preview simulates a strategy over the bars loaded in your browser: instant, but bounded by what the chart has fetched. Server-side backtesting runs the same strategy over a much deeper, authoritative history window on our servers, and renders the outcome back onto your chart.
Backtests use the exact same simulation engine as the preview: identical fill model (signals fill at the next bar's open, no lookahead), identical sizing and commission math, identical statistics. The same script over the same window produces the same trades in both places, to the last digit.
Running a backtest
A backtest runs a saved strategy script (a script that calls strategy(); indicator-only scripts are rejected with a clear message). In the Strategy tab of the script dock:
- Pick the saved script, a market, and a timeframe; these default to the chart you are viewing.
- Choose the window depth in bars (default 1,000, up to 10,000) and, optionally, a window end time; leave it empty to end at the most recently closed bar. Input overrides work exactly like alerts and the screener.
- Run. The stats panel and trade list appear in the tab; the equity curve is drawn in its own chart pane and every trade gets entry/exit markers, replacing the preview's overlays while the result is shown.
The history window
- Backtests run on closed bars only; a forming bar never enters the window.
- The window is guaranteed contiguous: if the venue's data has a hole in the requested range, the run fails with an explicit error instead of silently simulating over broken data.
- If the venue's history ends before the requested depth, the run completes on the bars that exist and is flagged "history limit reached": the window is shorter than requested, never padded.
- Deep windows are cached server-side, so re-running with different inputs over the same window is fast (results flagged as served from cache).
Limits
Backtesting is available on paid plans and is rate-limited rather than quota-counted:
- One backtest at a time per account, plus a short cooldown between runs; the Run button counts the wait down and re-enables itself.
- At most 10,000 bars per window. Assembling a deep, uncached window has its own time budget; if it runs out, you get an explicit error and a warmed cache makes the retry cheap.
- The simulation itself runs in the standard sandbox with budgets sized for deep windows; a script error is reported as-is, exactly like the preview.
Alerts
An alert turns a saved script into a server-side watcher: the script runs on our servers every time a bar closes for the market and timeframe you choose, and the alert fires whenever the script's alertcondition() marks that just-closed bar. Alerts keep working when your browser is closed.
Creating an alert
- Save your script (alerts run stored scripts, not unsaved drafts).
- Open the Alerts tab in the script dock and choose a market and timeframe; these default to the chart you are viewing.
- Set a cooldown and a delivery channel, adjust any input overrides, and create the alert.
Each alert can override the script's declared input.* values, so one script can power several alerts with different parameters (for example, an RSI script watched at length 14 on one alert and 21 on another).
Evaluation model
- Alerts are evaluated on closed bars only (the same semantics as the in-browser preview), so an alert reflects confirmed data, never a forming bar.
- Each bar is evaluated once. An alert fires only if
alertcondition()marks the most recently closed bar. - The script runs in the same sandbox as the browser preview and against the same built-in library, so an alert produces the same result your chart preview does.
Cooldown
Cooldown is the minimum time between two fires of the same alert. With a cooldown of, say, 3600 seconds, a condition that stays true on consecutive bars fires at most once per hour. A cooldown of 0 allows firing on every qualifying bar.
Delivery channels
- In-app: a live notification in the app (and the trigger feed in the Alerts tab).
- Email: an in-app notification plus an email for every trigger.
- Webhook: an in-app notification plus an HTTP POST to a URL you provide. The payload is customizable with an optional template and custom headers, every request is signed with a per-alert HMAC secret (readable from the alert's details, so the receiver can verify authenticity), and the alert's recent delivery attempts are listed with their status.
Reliability
If a script fails to evaluate repeatedly (for example, it throws), the alert is automatically disabled and shows the reason, so a broken script never fails silently. Re-enable it after fixing the script. Archiving a script disables the alerts bound to it.
Quotas
The number of scripts you can save and the number of enabled alerts you can run at once depend on your plan. Reaching a limit surfaces an upgrade prompt; disabling or deleting alerts frees quota.
Screener
The screener runs a saved script across many markets at once and lists the symbols that signal right now. Open the Screener tab in the script dock, pick a saved script and a timeframe, choose the markets to scan, and run.
What counts as a match
Each symbol is evaluated over its own recent closed-bar history, in the same sandbox and against the same built-in library as everywhere else. A symbol matches when the script marks that symbol's newest closed bar, either with alertcondition() (the message becomes the match text) or, for marker-style scripts, with signal() (the marker text is used). Decorative markers from plotshape() / plotchar() are not signals and never produce a match.
This is level-triggered ("which symbols satisfy the condition now") and intentionally differs from alerts, which are edge-triggered and fire only when a bar newly closes. A market that has been oversold for hours still shows up in an RSI screen.
Choosing the universe
The scan universe is the union of two sources, de-duplicated:
- Explicit symbols: a list of market keys or symbols (e.g.
BINANCE:SPOT:BTCUSDTor justETHUSDT), one per line or comma-separated. Symbols that don't resolve to a listed market are reported per-symbol, never silently dropped. - Catalog universe: a selector over the platform's market catalog, combining an optional search query (empty means the featured listings), a market-type filter (spot / perpetual / futures), an asset-class filter, and a result limit.
A quick (synchronous) run answers on the spot and scans at most 50 distinct symbols; larger universes run asynchronously with live progress, up to your plan's symbol cap (up to 1,500 symbols on the top plan). The form shows an upper-bound estimate as you type. Input overrides work exactly like alerts: the values you set replace the script's declared input.* defaults for the run.
A configured scan can also be stored as a saved scan and optionally put on a schedule (from every 15 minutes up to weekly), delivering its matches in-app, by email or to a webhook - the same channels alerts use. Like a broken alert, a scheduled scan that keeps failing is automatically disabled with its reason shown.
Limits and partial results
The screener is available on paid plans. Interactive runs are rate-limited, while saved and scheduled scans count against per-plan quotas (like scripts and alerts; reaching a quota surfaces an upgrade prompt):
- One run at a time per account, plus a short cooldown between completed runs. Running too soon returns a "try again" response; the Run button counts the remaining seconds down and re-enables itself.
- Each symbol's evaluation runs under the standard sandbox budgets; a script error on one symbol is reported for that symbol and never fails the batch.
- The whole run has a wall-clock budget. If the budget runs out (for example, on a cold cache), symbols not yet started are listed under skipped and the result is flagged partial; nothing is silently truncated. Running again a moment later typically completes, because the freshly cached candles make evaluation much faster.
Clicking a match switches the chart to that symbol so you can inspect the signal with the same script on the chart.
AI assistant
The Assistant tab in the script dock helps you write Kodexius Script in three modes:
- Generate: describe the indicator or strategy in plain language and get a complete script. The chart's current market and timeframe can be attached as context.
- Explain: get a concise plain-language explanation of the code currently in your editor.
- Convert from Pine: paste TradingView Pine Script and get an equivalent Kodexius Script.
The dry-run verdict
Generated and converted code is never handed to you untested: before returning, the assistant runs the code in the same sandbox your scripts run in, over a small synthetic candle window, and reports the verdict next to the code:
- Ran clean: the code compiled and executed; the badge also notes how many plots it produced and whether a strategy or alert conditions were detected.
- Failed: the code errored; you still get the code (often one small fix away), together with the error message.
Reviewing AI-produced code before trading on it is still your job: the verdict proves the code runs, not that the logic matches your intent.
Approximations in Pine conversions
Pine and Kodexius Script are not identical languages. When a Pine feature has no exact equivalent, the converter degrades explicitly instead of silently changing behavior: the affected line carries an // APPROXIMATION: comment describing what was substituted, and the panel lists every such note prominently. A conversion without approximation notes used only exact equivalents.
Applying results
From the result you can apply the code to the editor (with a confirmation when the editor holds unsaved changes) or load it as a new unsaved script; from there the normal preview/save/alert/backtest pipeline takes over.
The assistant is available on paid plans, handles one request at a time per account, and applies a short cooldown between requests (the button counts it down). Descriptions are capped at 4,000 characters and pasted code at 16,000.
Sharing scripts
Share links
Any saved script can be shared with a link (the Share button in the script dock header). Share links are read-only and anonymous-friendly:
- A link always serves the script's latest saved content: recipients see your current code, not a snapshot, and no account is needed to view it.
- The page shows only the script's name and code; nothing about your account is exposed.
- Creating a link again rotates it: a new URL is issued and the old one immediately stops working. Use this if a link leaked further than intended.
- Revoking the link makes it return "not found", indistinguishable from a link that never existed. Revocation is permanent; sharing again issues a fresh URL.
- Archived scripts cannot be shared, and archiving a script kills its active link.
Importing a shared script
The shared page's copy button imports the script into your own account as a new, independent script: your copy, your future edits, counted against your script quota like any other script. The imported code starts with a one-line // Imported from shared script provenance comment; nothing else is rewritten. After importing you land in the editor with the copy active, and the full preview/alert/screener/backtest toolchain applies.
Version history
Every save of a script appends an immutable version. The History button in the dock header lists them (newest first, with name, size and date), previews any version's code, and can restore one: restoring rewrites the script to that version's content and appends the restored content as a new version, so history is never rewritten and a restore can itself be undone the same way.
Security and sandbox limits
Scripts are treated as untrusted code and run inside a strict, isolated sandbox in your browser. The sandbox has no access to the page (DOM), cookies, local storage, or the network, and it runs off the main thread so it can never freeze the interface.
No network or file access
fetch, XMLHttpRequest, WebSocket, and any other network or file API are not defined in the sandbox. Calling them raises an error:
fetch("https://example.com"); // ReferenceError → reported as a script error
Determinism
The same script over the same data always produces the same result - in the browser preview and in every server-side run (alerts, screener, backtests) alike. Sources of non-determinism, string compilation and BigInt are therefore disabled in both runtimes:
Math.random(); // Error: "Math.random() is disabled (deterministic sandbox)"
Date.now(); // Error: "Date.now() is disabled (deterministic sandbox)"
eval("1 + 1"); // Error: "eval() is disabled (deterministic sandbox)"
new Function(""); // Error: "new Function() (string compilation) is disabled (deterministic sandbox)"
BigInt(1); // Error: "BigInt is disabled (deterministic sandbox)"
If you need time information, use the time series (bar open times, in Unix seconds).
Resource limits
| Limit | Default | Behavior |
|---|---|---|
| Execution time | 2000 ms | Scripts that run longer are stopped (infinite loops cannot hang the app). |
| Memory | 64 MiB | Scripts that allocate more are stopped with an out-of-memory error. |
A script that exceeds a limit does not run; the panel shows the error.
Drawing object limits
Drawing functions (line, label, box, table, polyline, volumeProfile) are capped per run. Exceeding a cap stops the script with a clear message (for example, line() limit exceeded: max 500 per run).
| Limit | Default | Notes |
|---|---|---|
| Lines | 500 | Enough for dense support/resistance or per-pivot annotation across a full screen of bars. |
| Labels | 500 | Enough for per-pivot labeling. |
| Boxes | 500 | Enough for zone/range boxes. |
| Tables | 5 | Roughly one info panel per corner plus a spare. |
| Table cells | 400 total | Covers a ~20×20 grid across all tables in a run. |
| Polylines | 200 | Independent paths per run. |
| Points per polyline | 2,000 | One path can trace a full screen of bars several times over. |
| Polyline points, total | 20,000 | Across all polylines in a run. |
| Volume profiles | 20 | For example, one per session across a month of daily sessions. |
| Buckets per profile | 200 | Price resolution of one histogram. |
Known limitations
These affect what a script can currently do:
- No
importor npm packages. Only the built-in API is available. - No order execution and no external data. Scripts are for analysis and signals only.
volumeis as reported by the venue. Charts carry real traded volume; in the rare case a venue reports no volume for a market, thevolumeseries contains zeros there.- Time is in seconds. The
timeseries and every time-taking API (str.format_time, drawing coordinates,input.time) use Unix seconds, never milliseconds. - Marker text shares the marker color. The chart's marker plugin draws a marker's glyph and text in one color, so
plotshape/plotcharhave no separate text color; uselabelfor independently colored text. barcolorneeds a candle-like main chart. Per-bar recolors apply when the main series renders candles or bars; line/area chart types have no per-bar body to color. While the current bar is still forming, a live tick updates only that bar, so closed-bar colors persist and re-apply on each bar close.- No timezone database.
str.format_timeaccepts"UTC"and fixed offsets ("UTC+3","-05:30"); named zones are rejected because resolving them would need a timezone database the deterministic sandbox does not carry. - Complex eigenvalues are rejected.
matrix.eigenvalues/matrix.eigenvectorssupport real spectra only and throw otherwise, rather than fabricating values.
Example scripts
The following scripts are complete and runnable. Some are also available from the Load example menu in the editor.
1. SMA cross (buy/sell signals)
indicator("SMA Cross", { overlay: true });
const fastLen = input.number("Fast length", 9, { min: 1, max: 200 });
const slowLen = input.number("Slow length", 21, { min: 1, max: 400 });
const fast = sma(close, fastLen);
const slow = sma(close, slowLen);
plot(fast, { title: "Fast SMA", color: "#22d3ee" });
plot(slow, { title: "Slow SMA", color: "#f59e0b" });
signal(crossover(fast, slow), { position: "belowBar", shape: "arrowUp", color: "#34d399", text: "BUY" });
signal(crossunder(fast, slow), { position: "aboveBar", shape: "arrowDown", color: "#f87171", text: "SELL" });
2. RSI with overbought/oversold alerts
indicator("RSI", { overlay: false });
const len = input.number("RSI length", 14, { min: 2, max: 100 });
const r = rsi(close, len);
plot(r, { title: "RSI", color: "#a78bfa" });
// Draw constant reference lines by mapping the series to a fixed value.
plot(r.map(() => 70), { title: "Overbought", color: "#f87171" });
plot(r.map(() => 30), { title: "Oversold", color: "#34d399" });
alertcondition(crossunder(r, 30), "RSI dropped below 30");
alertcondition(crossover(r, 70), "RSI rose above 70");
3. Bollinger Bands
indicator("Bollinger", { overlay: true });
const len = input.number("Length", 20, { min: 2, max: 200 });
const mult = input.number("Multiplier", 2, { min: 0.5, max: 5, step: 0.1 });
const basis = sma(close, len);
const dev = stdev(close, len);
const upper = basis.map((b, i) => b + mult * dev[i]);
const lower = basis.map((b, i) => b - mult * dev[i]);
plot(upper, { title: "Upper", color: "#f59e0b" });
plot(basis, { title: "Basis", color: "#94a3b8" });
plot(lower, { title: "Lower", color: "#f59e0b" });
4. ATR volatility (histogram, separate pane)
indicator("ATR", { overlay: false });
const len = input.number("ATR length", 14, { min: 1, max: 100 });
plot(atr(len), { title: "ATR", color: "#60a5fa", type: "histogram" });
5. Donchian breakout
indicator("Donchian Breakout", { overlay: true });
const len = input.number("Channel length", 20, { min: 2, max: 200 });
// Shift by one bar to use the channel up to the previous bar.
const upper = highest(high, len);
const lower = lowest(low, len);
const upperPrev = upper.map((v, i) => (i > 0 ? upper[i - 1] : NaN));
const lowerPrev = lower.map((v, i) => (i > 0 ? lower[i - 1] : NaN));
plot(upper, { title: "Upper channel", color: "#f59e0b" });
plot(lower, { title: "Lower channel", color: "#f59e0b" });
signal(crossover(close, upperPrev), { position: "belowBar", shape: "arrowUp", color: "#34d399", text: "Breakout" });
signal(crossunder(close, lowerPrev), { position: "aboveBar", shape: "arrowDown", color: "#f87171", text: "Breakdown" });
alertcondition(crossover(close, upperPrev), "Price broke the upper channel");
6. Support/resistance with pivot labels (drawing objects)
Uses line, box, and label to draw the range of the last N bars and label its edges.
indicator("Support / Resistance", { overlay: true });
const len = input.number("Range (bars)", 50, { min: 5, max: 300 });
const n = close.length;
const from = Math.max(0, n - len);
const xStart = time[from];
const xEnd = time[n - 1];
const hi = last(highest(high, len));
const lo = last(lowest(low, len));
box({ x1: xStart, y1: hi, x2: xEnd, y2: lo, color: "#22d3ee", fillOpacity: 0.06, borderColor: "#22d3ee" });
line({ x1: xStart, y1: hi, x2: xEnd, y2: hi, color: "#f87171", width: 2, style: "dashed", extend: "right" });
line({ x1: xStart, y1: lo, x2: xEnd, y2: lo, color: "#34d399", width: 2, style: "dashed", extend: "right" });
label({ x: xEnd, y: hi, text: "Resistance " + hi.toFixed(2), color: "#7f1d1d", textColor: "#fecaca", position: "top" });
label({ x: xEnd, y: lo, text: "Support " + lo.toFixed(2), color: "#064e3b", textColor: "#bbf7d0", position: "bottom" });
7. Stats table (corner panel)
Uses table to pin a summary panel to a chart corner. Cell text colors can be conditional.
indicator("Summary", { overlay: true });
const rsiLen = input.number("RSI length", 14, { min: 2, max: 100 });
const r = last(rsi(close, rsiLen));
const a = last(atr(14));
const lastPrice = last(close);
const trend = last(sma(close, 20)) < last(close) ? "Up" : "Down";
table({
position: "top-right",
color: "#0a0a0a",
borderColor: "#334155",
rows: [
[{ text: "Metric", textColor: "#94a3b8" }, { text: "Value", textColor: "#94a3b8", align: "right" }],
["Last price", { text: lastPrice.toFixed(2), align: "right" }],
["RSI(" + rsiLen + ")", { text: r.toFixed(1), align: "right", textColor: r > 70 ? "#f87171" : r < 30 ? "#34d399" : "#e5e7eb" }],
["ATR(14)", { text: a.toFixed(2), align: "right" }],
["Trend", { text: trend, align: "right", textColor: trend === "Up" ? "#34d399" : "#f87171" }],
],
});
8. Trend channel (polyline)
Samples rolling highs/lows into two polyline paths and shades the area between them with a filled polygon (the upper path plus the lower path reversed).
indicator("Trend Channel", { overlay: true });
const len = input.number("Channel length (bars)", 60, { min: 10, max: 300 });
const n = close.length;
const from = Math.max(0, n - len);
const hi = highest(high, 10);
const lo = lowest(low, 10);
const upper = [];
const lower = [];
for (let i = from; i < n; i += 5) {
if (isFinite(hi[i])) upper.push({ x: time[i], y: hi[i] });
if (isFinite(lo[i])) lower.push({ x: time[i], y: lo[i] });
}
if (upper.length >= 2 && lower.length >= 2) {
polyline({ points: upper.concat(lower.slice().reverse()), fill: true, fillColor: "#22d3ee", fillOpacity: 0.08, width: 1, style: "dotted" });
polyline({ points: upper, color: "#f87171", width: 2 });
polyline({ points: lower, color: "#34d399", width: 2 });
}
9. Volume profile
Volume-by-price histogram of the loaded range with the point of control and value area highlighted.
indicator("Volume Profile", { overlay: true });
const bins = input.number("Buckets", 24, { min: 4, max: 100 });
volumeProfile({
bins,
side: "right",
widthFraction: 0.25,
valueArea: 0.68,
color: "#3b82f6",
valueAreaColor: "#22d3ee",
pocColor: "#f59e0b",
});
10. SMA cross strategy
A complete strategy: signals fill at the next bar's open, the equity curve renders in its own pane, and statistics appear in the Strategy tab.
strategy({ title: "SMA Cross Strategy", initialCapital: 10000, sizing: { type: "percent", value: 95 }, commissionBps: 10 });
const fastLen = input.number("Fast length", 9, { min: 1, max: 200 });
const slowLen = input.number("Slow length", 21, { min: 1, max: 400 });
const fast = sma(close, fastLen);
const slow = sma(close, slowLen);
plot(fast, { title: "Fast SMA", color: "#22d3ee" });
plot(slow, { title: "Slow SMA", color: "#f59e0b" });
strategy.entry("trend", "long", { when: crossover(fast, slow) });
strategy.close("trend", { when: crossunder(fast, slow) });
What's next
The platform keeps evolving, and the API you write against stays stable: new capabilities arrive additively, as the richer strategy orders once promised here did - limit/stop/stop-limit entries, bracket exits with trailing stops and partial quantities, pyramiding, OCA groups and risk rules are all shipped and documented in Strategies, alongside server-side backtesting, multi-timeframe data, webhooks and scheduled scans.
Scripts you write today will continue to work unchanged as new features arrive.