feature
Definition examples
Three runnable definition scripts, simple → complex. Each marks setups for Collect — they do not measure outcomes (that is a study).
In Code the API word is flow (kind: "flow"). In the product UI the noun is always Definition.
Simple → complex
# | Example | Teaches |
|---|---|---|
1 | Bull bar |
|
2 | EMA cross |
|
3 | Session opening-range break | Session object, OR state, |
1. Bull bar
Marks every bar that closes above its open. Payload carries bar range for later studies.
What it defines
A Collectable event bull_bar whenever close > open.
Key decisions
Piece | Why it matters |
|---|---|
| Enables emit / Collect (not an indicator). |
| Contract for Collect and studies. |
Required | Events without labels are dropped at runtime. |
Early | Keep |
Full script
meta({
name: "Bull bar",
kind: "flow",
slug: "bull-bar",
shortName: "BullBar",
});
events.declare({
id: "bull_bar",
intent: "Mark closes above open",
payload: {
range: { type: "number", description: "High minus low" },
close: { type: "number", description: "Close price" },
},
paint: "pin",
});
outputs.declare({
id: "close_line",
kind: "line",
name: "Close",
color: "#64b5f6",
});
export function onBar(ctx) {
ctx.plot("close_line", ctx.close);
if (ctx.close <= ctx.open) return;
const range = ctx.high - ctx.low;
ctx.emit(
"bull_bar",
{ range, close: ctx.close },
{
label: `Bull · R=${range.toFixed(2)}`,
subtitle: `close ${ctx.close.toFixed(2)}`,
price: ctx.close,
timeStart: ctx.time,
}
);
}2. EMA cross
Fires when a fast EMA crosses above a slow EMA. Uses the public Moving Average indicator as a dependency instead of re-implementing EMA math.
What it defines
Event ema_cross_up on the bar where fast MA crosses from at-or-below slow MA to above it.
Key decisions
Piece | Why it matters |
|---|---|
Two | Same slug ( |
Guard | Deps are empty during warmup — skip, do not emit. |
Read-then-write | Identity read returns last bar’s values; second call stores today’s. |
| At most one emit per session window (optional throttle). |
Full script
meta({
name: "EMA cross up",
kind: "flow",
slug: "ema-cross-up",
shortName: "EmaXUp",
});
indicators.declare({
slug: "ma",
as: "fast",
version: 1,
settings: { period: 9, maType: "EMA" },
});
indicators.declare({
slug: "ma",
as: "slow",
version: 1,
settings: { period: 21, maType: "EMA" },
});
events.declare({
id: "ema_cross_up",
intent: "Fast EMA crosses above slow EMA",
payload: {
fast: { type: "number", description: "Fast EMA" },
slow: { type: "number", description: "Slow EMA" },
close: { type: "number", description: "Close at cross" },
},
paint: "pin",
oncePer: { keys: ["session"], windowBars: 1 },
});
outputs.declare({ id: "fast_line", kind: "line", name: "Fast", color: "#26a69a" });
outputs.declare({ id: "slow_line", kind: "line", name: "Slow", color: "#ef5350" });
export function onBar(ctx) {
const fast = ctx.indicators.fast?.value;
const slow = ctx.indicators.slow?.value;
if (fast == null || slow == null) return;
ctx.plot("fast_line", fast);
ctx.plot("slow_line", slow);
// Identity update returns the stored prior values without changing them.
const prevFast = ctx.accum("prevFast", null, (prev) => prev);
const prevSlow = ctx.accum("prevSlow", null, (prev) => prev);
if (prevFast != null && prevSlow != null && prevFast <= prevSlow && fast > slow) {
ctx.emit(
"ema_cross_up",
{ fast, slow, close: ctx.close },
{
label: "EMA cross ↑",
subtitle: `${fast.toFixed(2)} > ${slow.toFixed(2)}`,
price: ctx.close,
timeStart: ctx.time,
}
);
}
ctx.accum("prevFast", null, () => fast);
ctx.accum("prevSlow", null, () => slow);
}Pin version to the library MA version you resolve in the builder when you can — agents should call resolve_dependency_contract before inventing shapes. See Declaring indicator dependencies.
3. Session opening-range break
Builds a session opening range from the first N bars after session open, then emits once when price breaks the high (or low) after the range is locked.
What it defines
Events or_break_up / or_break_dn with the OR high/low and break price — one break direction per session via oncePer.
Key decisions
Piece | Why it matters |
|---|---|
| Definition session object ( |
OR state in | Remembers high/low/bar count across bars; resets on |
Emit only after range locked | Avoid firing during the building window. |
Separate event ids + | One up and one down break max per session, no hand-rolled dedupe. |
Full script
meta({
name: "OR break",
kind: "flow",
slug: "or-break",
shortName: "ORBreak",
});
events.declare({
id: "or_break_up",
intent: "Close breaks above session opening range",
payload: {
orHigh: { type: "number", description: "Opening range high" },
orLow: { type: "number", description: "Opening range low" },
close: { type: "number", description: "Break close" },
sessionKey: { type: "string", description: "Session key" },
},
paint: "pin",
oncePer: { keys: ["session"] },
});
events.declare({
id: "or_break_dn",
intent: "Close breaks below session opening range",
payload: {
orHigh: { type: "number", description: "Opening range high" },
orLow: { type: "number", description: "Opening range low" },
close: { type: "number", description: "Break close" },
sessionKey: { type: "string", description: "Session key" },
},
paint: "pin",
oncePer: { keys: ["session"] },
});
outputs.declare({ id: "or_high", kind: "line", name: "OR high", color: "#26a69a" });
outputs.declare({ id: "or_low", kind: "line", name: "OR low", color: "#ef5350" });
const OR_BARS = 5;
export function onBar(ctx) {
const s = ctx.session({});
const st = ctx.accum("or", null, (prev) => {
if (s.didOpen || prev == null) {
return {
key: s.key,
bars: 1,
high: ctx.high,
low: ctx.low,
locked: false,
};
}
if (prev.locked) return prev;
const bars = prev.bars + 1;
const high = Math.max(prev.high, ctx.high);
const low = Math.min(prev.low, ctx.low);
return {
key: s.key,
bars,
high,
low,
locked: bars >= OR_BARS,
};
});
if (!st.locked) return;
ctx.plot("or_high", st.high);
ctx.plot("or_low", st.low);
const base = {
orHigh: st.high,
orLow: st.low,
close: ctx.close,
sessionKey: st.key,
};
if (ctx.close > st.high) {
ctx.emit("or_break_up", base, {
label: "OR break ↑",
subtitle: `H ${st.high.toFixed(2)}`,
price: ctx.close,
timeStart: ctx.time,
});
} else if (ctx.close < st.low) {
ctx.emit("or_break_dn", base, {
label: "OR break ↓",
subtitle: `L ${st.low.toFixed(2)}`,
price: ctx.close,
timeStart: ctx.time,
});
}
}Tune SessionOpts in IntelliSense for your market (exchange RTH vs named London/NY sessions). See Sessions.
