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#
// 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.
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 heightUse rgba() colors for translucent zone fills. Plots:
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, nofetch, no timers —computemust return its result directly. - Single symbol. Scripts only receive the chart's own bars. No
cross-symbol or cross-timeframe data (Pine
request.securitycannot be ported). - No main-series restyling. Pine
plotcandle/barcolorhave no equivalent. - No persistence. Model "current state" by replaying the rules over the
series inside
compute(see the signal recipe).
Recipes#
Oscillator (separate pane)#
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.
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.
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:
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 / crossunder → boolean[] |
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#
- Write the script (top-level
input.*declarations +compute). - In the terminal: Indicators → My scripts → Create script, paste, choose Overlay or Separate pane, then Save & add to chart.
- Compile or runtime errors appear inline in the pane in red — fix and resave. A budget-exceeded script stays disabled until edited.
- Settings changes in the UI re-run
computelive; verify inputs by watching the legend values update.
