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 |
|
2 | Forward return |
|
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 |
|---|---|
| Binds Collect data; |
| Scratch buckets — not the same as |
| Aggregates after every event has been seen. |
Guard | 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 |
|---|---|
| Trader-tunable look-ahead without editing code. |
| One bar at/after the horizon (see Code IntelliSense for other shapes). |
Entry from | Must match what the definition emitted. |
| 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 |
|---|---|
| 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 |
|
| 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.
