ChartnautDocs

Stop / target first hit

After each opening range break, sets a hypothetical target and stop one opening range away from the break close and asks which one price reached first. It reports the rates overall and for up and down breaks separately.

What it measures

For every or_break_up and or_break_dn: did price reach the target, the stop, or neither before the scan ended. The distance is the opening range height times a multiple the trader sets.

Key decisions

Piece

Why

direction scope dimension

Known the moment the break fires, so the results split by it and a Forward Insight can show the reading for the break you click.

Payload check in scope

A break with no usable range returns null and is not part of the sample. Everything in onEvent may fail without dropping the event.

Distance from orHigh - orLow

Risk scales with the day's range instead of a fixed tick count.

stopAt + targetAt + firstHit: true

The runtime walks forward bar by bar and returns the bar where a level was touched, with hit set to "target", "stop" or "neither".

onEvent(ctx, scope)

Reads scope.direction from the tuple scope returned, so the group is decided once.

Table keyed by direction

indexedBy: ["direction"], and each row sets direction: explicitly.

Full script

meta({
  name: "OR break first hit",
  kind: "study",
  params: {
    rMultiple: { type: "number", def: 1, min: 0.25, max: 5, step: 0.25 },
  },
});

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

dimensions.declare({
  id: "direction",
  role: "scope",
  title: "Break direction",
  description: "Which side of the opening range the close broke.",
  values: ["up", "down"],
});

results.declare({
  id: "readout__summary",
  kind: "prose",
  role: "answer",
  title: "Summary",
  description: "Which level price usually reached first after a break.",
});

results.declare({
  id: "readout__events",
  kind: "metric",
  title: "Breaks",
  description: "Opening range breaks with at least one bar after the break.",
});

results.declare({
  id: "readout__target_rate",
  kind: "metric",
  title: "Target first",
  description: "Share of breaks where the hypothetical target was touched before the stop.",
});

results.declare({
  id: "by_direction",
  kind: "table",
  title: "By break direction",
  description: "Target-first and stop-first rates for up and down breaks.",
  indexedBy: ["direction"],
});

function levels(ctx) {
  const entry = Number(ctx.event.payload.close);
  const risk = Number(ctx.event.payload.orHigh) - Number(ctx.event.payload.orLow);
  return { entry, risk };
}

export function scope(ctx) {
  if (ctx.event.flow !== "orb") return null;
  if (ctx.event.id !== "or_break_up" && ctx.event.id !== "or_break_dn") return null;

  const { entry, risk } = levels(ctx);
  if (!Number.isFinite(entry) || !Number.isFinite(risk) || risk <= 0) return null;

  return { direction: ctx.event.id === "or_break_up" ? "up" : "down" };
}

export function onEvent(ctx, scope) {
  const { entry, risk } = levels(ctx);
  const direction = scope.direction;
  ctx.mark(ctx.event.time, entry, { color: direction === "up" ? "#26a69a" : "#ef5350", shape: "circle" });

  const dist = risk * ctx.params.rMultiple;
  const targetAt = direction === "up" ? entry + dist : entry - dist;
  const stopAt = direction === "up" ? entry - dist : entry + dist;

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

  ctx.collect("rows", { direction, hit: res.hit });
}

function share(rows, hit) {
  return rows.length ? rows.filter((r) => r.hit === hit).length / rows.length : 0;
}

export function onFinish(ctx) {
  const rows = ctx.collected("rows");
  const up = rows.filter((r) => r.direction === "up");
  const down = rows.filter((r) => r.direction === "down");

  ctx.publish("by_direction", [
    { direction: "up", breaks: up.length, target_first_pct: share(up, "target") * 100, stop_first_pct: share(up, "stop") * 100 },
    { direction: "down", breaks: down.length, target_first_pct: share(down, "target") * 100, stop_first_pct: share(down, "stop") * 100 },
  ]);

  const targetRate = share(rows, "target");
  ctx.publish("readout__events", { value: rows.length, format: "integer" });
  ctx.publish("readout__target_rate", { value: targetRate, format: "percent", n: rows.length });
  ctx.publish("readout__summary", {
    body: rows.length
      ? `${rows.length} breaks. The target was touched first in ${(targetRate * 100).toFixed(0)}% of them. These are hypothetical levels, not orders.`
      : "No breaks in this run.",
    role: "answer",
  });
}

"neither" means the scan reached the end of the loaded data, or its bar limit, without touching either level. On a long run those are usually the most recent breaks.

This study can be attached to the chart, and Break down by offers direction. See Show studies on charts.