Documentation

Writing Hyperprop Script indicators

Built for AI agents. This page is an agent skill: hand it to Cursor, Claude, or any coding assistant and it can write Hyperprop Script indicators for you. Grab the raw file at /hyperprop-script-skill.md or copy it from the scripting section on the home page.

Hyperprop Script is plain JavaScript, not Pine. A script declares its settings at the top level and defines one function, compute, that receives the full bar series and returns everything to draw. There is no per-bar execution model and no persistent state between runs — compute is a pure function of (bars, inputs).

Scripts are created in the web terminal: Indicators → My scripts → Create script. Errors never break the chart; they render as a red inline message in the pane. Scripts sync to the user's other devices (including mobile) automatically.

The contract#

js
// Top level: declare settings. The settings dialog is generated from these.
const length = input.int('Length', 14, { min: 1, max: 500 });
const color  = input.color('Line color', '#f59e0b');

// Called on every data change with the FULL series.
function compute(bars, inputs, hp) {
  // bars: Array<{ time: Date, o, h, l, c, v? }>
  // inputs: resolved settings values (rarely needed — the declared consts
  //         above already hold live values)
  // hp: TA helper bundle (see below)
  return {
    plots: [ /* required key, may be [] */ ],
    boxes: [], labels: [], lines: [], markers: [], bg: [],   // all optional
    range: { min: 0, max: 100 },   // optional: fix a separate pane's scale
    guides: [30, 70],              // optional: dashed horizontal levels
    decimals: 2,                   // optional: axis/legend precision
  };
}

Pane type is chosen at save time: overlay (drawn over price, y = price) or separate (own pane below price, y = the script's own value scale).

Input declarations#

All return the resolved live value. opts = { key?, min?, max?, step? }. Keys default to a slug of the label; pass key to keep saved settings stable across label renames.

Declaration Returns
input.int(label, default, opts?) integer
input.float(label, default, opts?) number
input.bool(label, default?) boolean
input.color(label, default) color string (hex or rgba())
input.string(label, default?) string
input.select(label, default, choices) string (choices: string[] or {value,label}[])
input.source(label?, default?) one of open/high/low/close/hl2/hlc3/ohlc4

TA helpers (hp)#

All series helpers take and return Array<number | null> aligned to bars. Nulls mark "no value yet" — always null-check before arithmetic.

Helper Notes
hp.sma / ema / rma / stdev (values, length) moving averages / deviation
hp.highest / lowest (values, length) rolling extremes
hp.change(values, length = 1) difference vs N bars back
hp.atr(bars, length) takes bars, not values
hp.rsi(values, length) 0–100
hp.linreg(values, length) least-squares regression value (Pine ta.linreg offset 0)
hp.cum(values) running sum
hp.vwap(bars) day-anchored VWAP (resets on UTC date change)
hp.pivothigh / pivotlow (values, left, right = left) pivot VALUE emitted at the pivot bar itself (null elsewhere). Unlike Pine, which reports it right bars later — a pivot at index p is only knowable from bar p + right onward; account for that in signal logic.
hp.crossover / crossunder (a, b) returns boolean[]; b may be a series or a constant

Anything else is ordinary JavaScript — write your own loops and math freely.

Drawing primitives#

X coordinates are bar indices into bars. Fractional values and indices past the last bar are fine (the axis extrapolates), so zones and rays can extend into the future. Y is a price (overlay) or the script's own value (separate pane). Boxes and bg bands render under the series; lines, markers, and labels render on top. Everything clips to the pane. Hard cap: 2000 items per type — return only what is currently visible/alive, never the full history.

js
boxes:   [{ x1, y1, x2, y2, bg?, border?, borderWidth?, text?, textColor? }]
labels:  [{ x, y, text, color?, textColor?, anchor?, size? }]
         // anchor: 'up' (bubble above point) | 'down' | 'left' | 'right' | 'center'
lines:   [{ x1, y1, x2, y2, color?, width?, style? }]   // style: solid|dashed|dotted
markers: [{ x, y, shape?, color?, text?, textColor? }]
         // y: number, or 'above'/'below' → anchors to the bar's high/low
         // shape: triangle-up|triangle-down|circle|square|arrow-up|arrow-down
bg:      [{ x1, x2, color }]   // vertical band, full pane height

Use rgba() colors for translucent zone fills. Plots:

js
plots: [{ title?, values, color?, width?, style?: 'line' | 'histogram', negativeColor? }]

Hard limits — design around these#

  • 1000 ms execution budget per compute, enforced with loop guards. A script that exceeds it is circuit-broken until edited. Keep hot loops O(bars); avoid O(bars²) rescans.
  • Synchronous only. No async, no fetch, no timers — compute must return its result directly.
  • Single symbol. Scripts only receive the chart's own bars. No cross-symbol or cross-timeframe data (Pine request.security cannot be ported).
  • No main-series restyling. Pine plotcandle / barcolor have no equivalent.
  • No persistence. Model "current state" by replaying the rules over the series inside compute (see the signal recipe).

Recipes#

Oscillator (separate pane)#

js
const length = input.int('Length', 14, { min: 2, max: 100 });
function compute(bars, inputs, hp) {
  const rsi = hp.rsi(bars.map(b => b.c), length);
  return {
    plots: [{ title: 'RSI', values: rsi, color: '#ab47bc' }],
    range: { min: 0, max: 100 },
    guides: [30, 70],
  };
}

Zones / order blocks (overlay, boxes + markers)#

The core pattern for supply/demand, FVG, and session-level indicators: build each zone's lifecycle (created / mitigated), then emit boxes only for zones still alive at the last bar.

js
const lookback = input.int('Lookback (bars)', 10, { min: 3, max: 50 });
function compute(bars, inputs, hp) {
  const highs = bars.map(b => b.h), lows = bars.map(b => b.l);
  const ph = hp.pivothigh(highs, lookback), pl = hp.pivotlow(lows, lookback);
  const boxes = [], last = bars.length - 1;
  for (let p = 0; p < bars.length; p += 1) {
    if (pl[p] == null && ph[p] == null) continue;
    const bull = pl[p] != null;
    const top = highs[p], bottom = lows[p];
    let mitigated = null;
    for (let j = p + lookback; j <= last; j += 1) {
      if (bull ? lows[j] < bottom : highs[j] > top) { mitigated = j; break; }
    }
    if (mitigated != null) continue; // only zones still alive
    boxes.push({
      x1: p, y1: top, x2: last + 20, y2: bottom, // extends into the future
      bg: bull ? 'rgba(0,200,83,0.18)' : 'rgba(211,47,47,0.18)',
      border: bull ? '#00c853' : '#d32f2f',
      text: bull ? 'demand' : 'supply',
    });
  }
  return { plots: [], boxes };
}

Signal engine (entry/SL/TP with history)#

Pine keeps var state across bars; here, replay the state machine in one sequential loop. One active signal at a time; finished signals become faded markers.

js
let active = null; const history = [];
for (let i = 1; i < bars.length; i += 1) {
  if (active) {
    const slHit = active.bull ? lows[i] <= active.sl : highs[i] >= active.sl;
    const tpHit = active.bull ? highs[i] >= active.tp : lows[i] <= active.tp;
    if (slHit || tpHit) { history.push(active); active = null; }
  }
  if (!active && /* entry condition at bar i */ false) {
    const entry = closes[i], sl = lows[i] - 3, tp = entry + (entry - sl) * 2;
    active = { bull: true, entry, sl, tp, bar: i };
  }
}
// history → translucent markers; active → dashed entry line + SL/TP lines
// + labels anchored at active.bar and active.bar + 55.

Session shading (bg bands)#

Intl.DateTimeFormat is available for timezone math:

js
const fmt = new Intl.DateTimeFormat('en-US',
  { timeZone: 'America/New_York', hour: 'numeric', minute: 'numeric', hour12: false });
const minutesNY = t => {
  const p = fmt.formatToParts(t);
  return (Number(p.find(x => x.type === 'hour').value) % 24) * 60
       + Number(p.find(x => x.type === 'minute').value);
};
// Walk bars, open a band when entering the window, close it when leaving:
// bg.push({ x1: openBar, x2: closeBar, color: 'rgba(230,81,0,0.09)' })
// Limit to recent history (e.g. last ~900 bars) to stay under the 2000 cap.

Porting from Pine Script#

Pine Hyperprop Script
input.int(14, "Length") input.int('Length', 14, { min, max })
ta.sma/ema/rma/stdev/atr/rsi/linreg hp. equivalents (note hp.atr(bars, len))
ta.pivothigh(high, l, r) hp.pivothigh(highs, l, r) — value at the pivot bar, not confirmation bar
ta.crossover / crossunder hp.crossover / crossunderboolean[]
close[3] (history operator) closes[i - 3] inside your own loop
var x = … + per-bar mutation sequential for loop over bars inside compute
box.new / label.new / line.new push to boxes / labels / lines arrays
plotshape push to markers
bgcolor / session boxes push to bg
fill(p1, p2) no fill — plot the two boundary lines, or per-segment boxes
plotcandle / barcolor not supported
request.security not supported (single symbol only)
alertcondition not supported (price alerts are a separate terminal feature)
xloc.bar_time timestamps bar indices (extrapolate past the last bar for "future" x)

Pine's per-bar drawing mutation (create → move → delete) collapses into one question: what should be visible right now? Compute final state once.

Workflow#

  1. Write the script (top-level input.* declarations + compute).
  2. In the terminal: Indicators → My scripts → Create script, paste, choose Overlay or Separate pane, then Save & add to chart.
  3. Compile or runtime errors appear inline in the pane in red — fix and resave. A budget-exceeded script stays disabled until edited.
  4. Settings changes in the UI re-run compute live; verify inputs by watching the legend values update.