feature

Study examples

Three runnable study scripts, simple → complex. Studies consume definition events you already Collected — they never call ctx.emit.

Lifecycle is always onEvent (per matching event) then onFinish (aggregate and publish).

Simple → complex

#

Example

Teaches

1

Event count

flows.declare, collect / publish, metric + table

2

Forward return

ctx.candles.forward, distribution result

3

Stop / target first hit

Barrier scan, rates, session buckets

Point each study’s flows.declare.slug at a definition you have Collected (examples below assume the definition examples slugs).


1. Event count

Counts how often a definition fired and lists each event’s time + payload fields.

What it studies

Frequency of bull-bar events — a metric total and a row table.

Key decisions

Piece

Why it matters

flows.declare({ slug, as })

Binds Collect data; as is the ctx.event.flow name.

ctx.collect in onEvent only

Scratch buckets — not the same as results.declare ids.

ctx.publish in onFinish only

Aggregates after every event has been seen.

Guard ctx.event.flow !== "bull"

Ignore other flows if the run attaches more than one.

Full script

meta({ name: "Bull bar count", kind: "study" });

flows.declare({
  slug: "bull-bar",
  as: "bull",
});

results.declare({
  id: "summary",
  kind: "metric",
  title: "Event count",
});

results.declare({
  id: "rows",
  kind: "table",
  title: "Events",
});

export function onEvent(ctx) {
  if (ctx.event.flow !== "bull") return;
  ctx.collect("n", 1);
  ctx.collect("rows", {
    time: ctx.event.time,
    range: ctx.event.payload.range,
  });
}

export function onFinish(ctx) {
  const n = ctx.collected("n").length;
  ctx.publish("summary", { value: n, label: "Events" });
  ctx.publish("rows", ctx.collected("rows"));
}

2. Forward return

For each EMA-cross event, measures close-to-close return over a horizon, then publishes a distribution.

What it studies

Distribution of forward returns after ema-cross-up events.

Key decisions

Piece

Why it matters

ctx.params.horizonMin

Trader-tunable look-ahead without editing code.

ctx.candles.forward({ minutes })

One bar at/after the horizon (see Code IntelliSense for other shapes).

Entry from payload.close

Must match what the definition emitted.

kind: "distribution"

Result UI expects an array of numeric samples.

Full script

meta({
  name: "EMA cross forward return",
  kind: "study",
  params: {
    horizonMin: { type: "number", def: 60, min: 1, max: 1440 },
  },
});

flows.declare({
  slug: "ema-cross-up",
  as: "xup",
});

results.declare({
  id: "fwd_ret",
  kind: "distribution",
  title: "Forward returns",
});

results.declare({
  id: "n",
  kind: "metric",
  title: "Samples",
});

export function onEvent(ctx) {
  if (ctx.event.flow !== "xup") return;

  const entry = Number(ctx.event.payload.close);
  if (!Number.isFinite(entry) || entry === 0) return;

  const after = ctx.candles.forward({ minutes: ctx.params.horizonMin });
  if (!after || after.close == null) return;

  const ret = (after.close - entry) / entry;
  ctx.collect("rets", ret);
}

export function onFinish(ctx) {
  const rets = ctx.collected("rets");
  ctx.publish("n", { value: rets.length, label: "Samples" });
  ctx.publish("fwd_ret", rets);
}

Heavy per-event candle work can hit study budgets — see Budgets and limits.


3. Stop / target first hit

After an OR break, asks whether price hit a +R target or −R stop first, then reports hit rates overall and by session key.

What it studies

Win / loss / neither rates for symmetric barriers around the break price, plus a table sliced by sessionKey from the event payload.

Key decisions

Piece

Why it matters

stopAt / targetAt + firstHit: true

Runtime walks forward until one barrier (or neither).

Barriers from event price × R

Risk distance comes from study params, not hard-coded ticks.

Collect structured rows

onFinish can aggregate rates and session slices from one bucket.

hit: "target" \| "stop" \| "neither"

Matches the forward barrier result shape.

Full script

meta({
  name: "OR break first hit",
  kind: "study",
  params: {
    rTicks: { type: "number", def: 20, min: 1, max: 500 },
    tickSize: { type: "number", def: 0.25, min: 0.0001 },
  },
});

flows.declare({
  slug: "or-break",
  as: "orb",
});

results.declare({ id: "events", kind: "metric", title: "Events" });
results.declare({ id: "target_rate", kind: "metric", title: "Target rate" });
results.declare({ id: "stop_rate", kind: "metric", title: "Stop rate" });
results.declare({
  id: "by_session",
  kind: "table",
  title: "By session",
});

export function onEvent(ctx) {
  if (ctx.event.flow !== "orb") return;

  const entry = Number(ctx.event.payload.close);
  if (!Number.isFinite(entry)) return;

  const dist = ctx.params.rTicks * ctx.params.tickSize;
  const isUp = ctx.event.id === "or_break_up";
  const targetAt = isUp ? entry + dist : entry - dist;
  const stopAt = isUp ? entry - dist : entry + dist;

  const res = ctx.candles.forward({
    targetAt,
    stopAt,
    firstHit: true,
  });
  if (!res) return;

  ctx.collect("rows", {
    sessionKey: ctx.event.payload.sessionKey || "unknown",
    hit: res.hit,
    side: isUp ? "up" : "dn",
  });
}

export function onFinish(ctx) {
  const rows = ctx.collected("rows");
  const n = rows.length;
  const targets = rows.filter((r) => r.hit === "target").length;
  const stops = rows.filter((r) => r.hit === "stop").length;

  ctx.publish("events", { value: n });
  ctx.publish("target_rate", {
    value: n ? targets / n : 0,
    format: "percent",
  });
  ctx.publish("stop_rate", {
    value: n ? stops / n : 0,
    format: "percent",
  });

  const keys = [];
  for (const r of rows) {
    if (!keys.includes(r.sessionKey)) keys.push(r.sessionKey);
  }

  ctx.publish(
    "by_session",
    keys.map((sessionKey) => {
      const slice = rows.filter((r) => r.sessionKey === sessionKey);
      const sn = slice.length;
      const st = slice.filter((r) => r.hit === "target").length;
      return {
        sessionKey,
        events: sn,
        target_rate_pct: sn ? Number(((st / sn) * 100).toFixed(1)) : 0,
      };
    })
  );
}

Showing these numbers on the Terminal uses Put on chart / the pin orchestrator — not the study authoring agent. See Show studies on charts.

Next

  1. Collect and publish

  2. Results and stats

  3. First study guide