tickstreamdocs

DOCS

Custom indicators

Write your own indicators for the tickstream terminal in JavaScript: plot lines, histograms, markers and labels on the price chart or in a separate pane, from candles, live ticks and the footprint.

The tickstream terminal runs your own indicators. You write a few lines of JavaScript, press Run, and what the script plots is drawn on the price chart or in its own pane — on the same candles, ticks and footprint the terminal shows, updated live.

where it runs

Scripts execute in your browser, in a sandboxed Web Worker. Your code never runs on our servers, and a script cannot reach the network or the page. It sees exactly what the chart shows — the live session, or a stored session when you step back in time.

Open the editor

  1. Open the terminal and click LAYOUT in the top bar.
  2. Add the Script editor panel, and the Script output panel if your indicator draws below the chart.
  3. Pick an example from the list in the editor, or write your own, and press Run.
⌘/Ctrl + EnterRun the script
⌘/Ctrl + SSave it
TabIndent two spaces
API buttonA short reference, inside the editor

Signed in, scripts are saved to your account and follow you to any browser — up to 40 scripts of up to 64 KB each. Without an account they are kept in this browser only.

Your first script

A 20-bar simple moving average on the price chart.

// Runs once per candle, oldest first, and again for the live candle on every tick.
onBar((bar, i, $) => {
  plot("sma 20", ta.sma($.close, 20, i), { color: "#0099ff" });
});

That is the whole shape of an indicator: register a callback with onBar, compute a value for candle i, and hand it to plot. The status line under the editor confirms what ran — for example running · 1 series · 0 markers · 695 bars.

How a script runs

  1. Once, top to bottom. The script body runs a single time. Use it to read settings with input and to register callbacks.
  2. A full pass. Every onBar callback runs for every candle of the chart's timeframe, oldest first. Then every onFinish callback runs once.
  3. Live. On each price frame (up to ten a second) the last candle is updated, onBar runs again for that candle only, then every onTick callback runs.
  4. Reload. When the chart reloads its candles — every 30 seconds on the live session, and on every change of symbol or timeframe — the output is cleared and a full pass runs again.

What a callback draws is redrawn on every pass. Guide lines and log lines from the top-level body stay for the whole run — declare fixed levels there once.

The live candle

The one rule that matters: onBar is called many times for the live candle. State you add to in plain variables counts the same candle again on every tick. Compute from the arrays, use the ta helpers, or keep your own state per bar index.

// WRONG — onBar runs again for the live candle on every tick, so this keeps growing
let total = 0;
onBar((bar, i, $) => { total += bar.volume; plot("total", total, { overlay: false }); });

// RIGHT — a running sum by index is the same number however often the candle is re-run
onBar((bar, i, $) => { plot("total", ta.cum($.volume, i), { overlay: false }); });
// Anything you must remember yourself: keep it per index, overwrite rather than add.
const swing = [];
onBar((bar, i, $) => {
  swing[i] = i > 0 && bar.high > $.high[i - 1] ? (swing[i - 1] || 0) + 1 : 0;
  if (swing[i] === 5) marker("5↑", { at: "above" });
});

The data a script sees

Candles

The first argument of onBar is the candle, the second its index, the third ($) the whole series.

bar.timeCandle open time, unix seconds
bar.open · high · low · closeFutures price, in points
bar.volumeContracts traded in the candle
bar.deltaAggressive buy volume minus aggressive sell volume, from the exchange-reported aggressor side
$.close[i], $.open, $.high, $.low, $.volume, $.delta, $.timeThe same fields for every candle as typed arrays (Float64Array), index 0 = oldest
$.barsThe candles as objects
$.tfThe chart's timeframe in seconds (300 on 5m)
$.symbolThe chart's market: NQ, ES, GC, SI or CL
$.i / $.barInside a callback: the candle being computed

The series covers what the chart holds: the stored sessions behind the current one, followed by the session in progress.

Live ticks

onTick((tick, bar, $) => {
  // tick = { price, ts, n } — the latest price, its time, and how many prints this frame collapsed
  if (tick.n > 50) log("burst", tick.n, "prints at", tick.price);
});
tick.priceLatest traded price
tick.tsIts time, unix seconds
tick.nHow many prints the frame collapsed — frames are coalesced to at most ten a second

Footprint

fp(bar) returns the footprint rows of a candle, ascending by price: [{ price, buy, sell }] — lots that lifted the offer and lots that hit the bid at each price. Rows come from the live stream and cover up to the last eight hours; for older candles, and when the chart shows a stored session, the array is empty.

Drawing

plot(name, value, options) One value per candle for a named series. NaN or a missing value leaves a gap.
overlaytrue (default) on the price chart, false in the Script output pane
style"line" (default), "hist" or "dots"
color — any CSS colour; without one a colour is assigned per series
width — line width in pixels, default 1.4
plotted(name)A series' own values as an array — for crosses and look-backs on what you plotted
hline(value, { color, label, overlay })A dashed horizontal line across the chart (or the pane with overlay: false). Declare fixed levels in the top-level body; identical lines are drawn once
marker(text, { at, color })Text above (at: "above") or below (default) the current candle
label(text, price, { color })A tag at a price on the current candle
bgcolor(color)Shades the current candle's column
log(…values)Writes a line under the editor; objects are printed as JSON. Up to 200 lines per pass
input(name, default)Names a setting and returns its value. The terminal has no settings form for scripts yet, so this is the default — change it in the code

Tables

table(id, rows, { at, title }) draws a table on the chart, the way Pine's table.new is used for a stats box. rows is an array of rows, a row an array of cells, a cell a string or { text, color, bg }. The first column is left-aligned and dim, the rest right-aligned. at is "top-right" (default), "top-left", "bottom-left" or "bottom-right"; tables that share a corner stack. Up to 30 rows of 8 cells. Every call replaces the table with that id, so calling it from onBar keeps it current.

onBar((bar, i, $) => {
  const v = ta.vwap($, i), atr = ta.atr($, 14, i), p = position();
  table("stats", [
    ["vwap", (bar.close - v).toFixed(2) + " pts"],
    ["atr 14", atr.toFixed(2)],
    ["position", p.qty ? { text: p.qty + " @ " + p.avg, color: p.qty > 0 ? "#34d399" : "#ff5c6a" } : "flat"],
  ], { at: "top-right", title: $.symbol + " · " + $.tf / 60 + "m" });
});

Orders from a script

A script can place orders through the Trade panel — on the panel's contract and account, the same route as the Buy and Sell buttons. Because onBar also runs over the whole history and again on every tick, the rules are strict, and they are enforced by the terminal, not by your code:

  • Live ticks only. An order called during the pass over the history is ignored. Only a call made while a live tick is being handled leaves the script.
  • Once per candle. The same action on the same candle fires once, however many ticks arrive, unless you pass { repeat: true }.
  • Only while allow trading is on. The switch in the editor bar is off after every Run and every script change. Off, every order is written to the log with (not sent) — so you watch a strategy fire for as long as you like before it sends anything.
  • One order every two seconds, five contracts at most. Faster switches trading off, as does a script error or a refusal from the broker.
buy(qty, { stop, target, tag, repeat }) · sell(…)Market entry, with an optional bracket that rests at the broker as one OCO pair. tag is a note for the log.
close()Flattens the position in the panel's contract at market.
position(){ qty, avg, contract } — the open position on the panel's contract, refreshed with the panel (every 2.5 s). qty is negative when short, 0 when flat.

Everything else the Trade panel requires still applies: a linked account, the execution entitlement, and order placement enabled on the account.

const fast = input("fast", 9), slow = input("slow", 21);

onBar((bar, i, $) => {
  const f = ta.ema($.close, fast, i), s = ta.ema($.close, slow, i);
  plot("fast", f, { color: "#0099ff" }); plot("slow", s, { color: "#d39794" });
  const x = ta.cross(plotted("fast"), plotted("slow"), i), p = position();
  // buy() and close() only ever fire on a live tick, once per candle — a pass over the
  // history runs this same code for every candle and places nothing.
  if (x > 0 && p.qty === 0) buy(1, { stop: bar.close - 2 * ta.atr($, 14, i), target: bar.close + 4 * ta.atr($, 14, i), tag: "ema cross" });
  if (x < 0 && p.qty > 0) close();
});

Built-in calculations — ta

Every function computes its value at index i and is cached across the pass, so calling it on every candle and every tick is cheap. Pass arrays that live for the whole run — $.close, $.volume, plotted(…) — not arrays you build inside the callback.

ta.sma(arr, n, i) · ta.ema(arr, n, i)Simple and exponential moving average
ta.rsi(arr, n, i)Relative strength index, 0–100
ta.atr($, n, i)Average true range (takes $, it needs high, low and close)
ta.stdev(arr, n, i)Standard deviation over n values
ta.highest(arr, n, i) · ta.lowest(arr, n, i)Extreme over the last n values
ta.change(arr, i, k = 1)arr[i] − arr[i − k]
ta.cum(arr, i)Running sum from the first candle
ta.vwap($, i) · ta.vwapSd($, i)Session VWAP and its volume-weighted standard deviation, reset at each CME session (18:00 New York)
ta.session(i)An id for the CME session candle i belongs to — equal ids, same session
ta.cross(a, b, i)1 when a crosses above b at i, -1 below, else 0. Either side may be an array or a number

Values before a window is full (the first n − 1 candles of an SMA, say) are NaN, which plot leaves as a gap.

Examples

Each of these runs as is. Paste it into the editor and press Run.

Session VWAP with bands

VWAP that resets at each CME session, one-sigma bands, and a marker where price crosses back above it.

const k = input("sigma", 1);

onBar((bar, i, $) => {
  const v = ta.vwap($, i);
  const sd = ta.vwapSd($, i) * k;
  plot("vwap", v, { color: "#ebd5be", width: 1.6 });
  plot("upper", v + sd, { color: "rgba(235,213,190,.45)" });
  plot("lower", v - sd, { color: "rgba(235,213,190,.45)" });
  if (ta.cross($.close, plotted("vwap"), i) > 0) marker("↑", { at: "below", color: "#34d399" });
});

RSI in the output pane

A 14-bar RSI below the chart with 70/30 guides, and the candle shaded when it is stretched.

const n = input("length", 14);

hline(70, { overlay: false, label: "70", color: "#ff5c6a" });
hline(30, { overlay: false, label: "30", color: "#34d399" });

onBar((bar, i, $) => {
  const r = ta.rsi($.close, n, i);
  plot("rsi " + n, r, { overlay: false, color: "#ebd5be" });
  if (r > 70) bgcolor("#ff5c6a");
  if (r < 30) bgcolor("#34d399");
});

EMA cross with candle delta

Two EMAs on the chart, each candle's aggressor delta as a histogram in the pane, and a marker on every cross.

const fast = input("fast", 9), slow = input("slow", 21);

onBar((bar, i, $) => {
  plot("ema " + fast, ta.ema($.close, fast, i), { color: "#0099ff" });
  plot("ema " + slow, ta.ema($.close, slow, i), { color: "#d39794" });
  plot("delta", bar.delta, { overlay: false, style: "hist" });

  const x = ta.cross(plotted("ema " + fast), plotted("ema " + slow), i);
  if (x > 0) marker("▲", { at: "below", color: "#34d399" });
  if (x < 0) marker("▼", { at: "above", color: "#ff5c6a" });
});

ATR stop line and the day's range

A trailing 2×ATR line under the close, and a label with the session high-low range on the last candle.

const mult = input("atr multiple", 2);

onBar((bar, i, $) => {
  plot("atr stop", bar.close - mult * ta.atr($, 14, i), { style: "dots", color: "#f59e0b" });
});

onFinish(($) => {
  const last = $.bars.length - 1;
  let hi = -Infinity, lo = Infinity;
  for (let i = last; i >= 0 && ta.session(i) === ta.session(last); i--) {
    hi = Math.max(hi, $.high[i]);
    lo = Math.min(lo, $.low[i]);
  }
  log("session range", (hi - lo).toFixed(2), "points");
});

Stacked footprint imbalances

Counts diagonal bid/ask imbalances in each candle's footprint and marks candles with three or more. Needs the live stream.

const ratio = input("ratio", 3), minLots = input("min lots", 10);

onBar((bar, i, $) => {
  const rows = fp(bar);
  let up = 0, dn = 0;
  for (let k = 1; k < rows.length; k++) {
    if (rows[k].buy >= minLots && rows[k].buy >= ratio * Math.max(rows[k - 1].sell, 1)) up++;
    if (rows[k - 1].sell >= minLots && rows[k - 1].sell >= ratio * Math.max(rows[k].buy, 1)) dn++;
  }
  plot("imbalances", up - dn, { overlay: false, style: "hist" });
  if (up >= 3) marker(up, { at: "below", color: "#34d399" });
  if (dn >= 3) marker(dn, { at: "above", color: "#ff5c6a" });
});

Sandbox and limits

TimeA full pass may take up to 4 seconds. Longer, and the worker is stopped with stopped: a pass took longer than 4 s — a runaway loop cannot freeze a trading screen.
NetworkNone. fetch, XMLHttpRequest, WebSocket and importScripts are removed.
PageNo DOM and no access to the terminal page, your session or your key. A script can only draw through the functions above.
ErrorsA thrown error shows as error: … under the editor, with the message, and the chart keeps its other layers.
Library40 scripts per account, 64 KB each.
OrdersOnly on live ticks, once per candle, while allow trading is on — see orders from a script.

When something looks wrong

A value keeps growing on the live candleState added to on every call — see the live candle.
Nothing in the Script output paneThe series needs overlay: false, and the pane has to be in your layout.
fp(bar) is always emptyThe footprint comes from the live stream: it covers recent candles, and none on a stored session.
A line starts lateThe window is not full yet — an SMA of 200 draws from the 200th candle.