ChartnautDocs

Forward return

For each EMA cross up, measures the return from the cross close to the close a set number of minutes later, then publishes the spread of those returns.

What it measures

Forward return in basis points after ema_cross_up, over a horizon the trader sets before each run.

Key decisions

Piece

Why

params.horizonMin

The trader changes the horizon in the run dialog without editing code.

scope returns {}

The event joins the sample from what is known when it fires, even when the horizon bar is not loaded yet.

ctx.candles.forward({ minutes })

Returns the one bar at or just before event time plus the horizon, or nothing past the end of the data.

Entry from payload.close

The close the definition emitted at the cross.

ctx.priceUnits.inBP

Basis points compare across instruments and price levels.

kind: "distribution"

Takes a plain array of numbers.

Full script

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

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

results.declare({
  id: "readout__summary",
  kind: "prose",
  role: "answer",
  title: "Summary",
  description: "The typical move after an EMA cross up, in plain words.",
});

results.declare({
  id: "readout__events",
  kind: "metric",
  title: "Crosses measured",
  description: "EMA crosses up with a bar at the horizon.",
});

results.declare({
  id: "readout__median_bp",
  kind: "metric",
  title: "Median return (bp)",
  description: "Median close-to-close return over the horizon, in basis points.",
});

results.declare({
  id: "fwd_ret",
  kind: "distribution",
  size: "lg",
  title: "Forward returns (bp)",
  description: "Close-to-close return from the cross to the horizon bar, in basis points.",
});

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

export function onEvent(ctx, scope) {
  const entry = Number(ctx.event.payload.close);
  if (!Number.isFinite(entry) || entry === 0) return;
  ctx.mark(ctx.event.time, entry, { color: "#26a69a", shape: "circle" });

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

  ctx.collect("rets", ctx.priceUnits.inBP(after.close, entry));
}

export function onFinish(ctx) {
  const rets = ctx.collected("rets");
  const median = rets.length ? ctx.stat.median(rets) : 0;

  ctx.publish("readout__events", { value: rets.length, format: "integer" });
  ctx.publish("readout__median_bp", { value: median, n: rets.length });
  ctx.publish("fwd_ret", rets);
  ctx.publish("readout__summary", {
    body: rets.length
      ? `${rets.length} crosses. Median return after ${ctx.params.horizonMin} minutes: ${median.toFixed(1)} bp.`
      : "No crosses had a bar at the horizon.",
    role: "answer",
  });
}

Because it reads forward, this study can be attached to the chart. With no dimensions it pins as one global card. To get a reading per session or trend state, add a scope dimension as in Stop / target first hit.