Signals in SJS
Observe named Pine signals with explicit timestamp, freshness, and duplicate rules.
SJS runs before native Pine in the shared frame runner. A newly calculated Pine signal is therefore normally available to a later SJS invocation, not retroactively to the SJS code that already ran.
Choose whether your strategy needs a same-timestamp update, a recently completed signal, or a cached direction with an expiration rule. The following observer uses a recently produced nonzero event.
Observe a recent event
Pair this complete SJS script with the Pine indicator. It reads raw pine.signal plot points with time and value, accepts only numeric 1 or -1, and searches at most the latest 200 points.
export async function app({ context, bar, log }) {
const frame = bar?.date == null ? NaN : new Date(bar.date).getTime();
if (!Number.isFinite(frame)) return "waiting: invalid frame";
const points = context.pineState?.plots?.["pine.signal"]?.data;
if (!Array.isArray(points)) return "waiting: Pine output unavailable";
const maximumAgeMs = 120000;
const candidates = points.slice(-200).filter(point =>
typeof point?.time === "number" && Number.isFinite(point.time)
&& (point.value === 1 || point.value === -1)
&& point.time <= frame && frame - point.time <= maximumAgeMs
);
const signal = candidates.reduce((latest, point) =>
!latest || point.time >= latest.time ? point : latest, null);
if (!signal) return "waiting: no recent directional event";
const state = (context.sjsState ??= {}).pineSignalObserver ??= {};
if (state.lastTime != null && signal.time <= state.lastTime) {
return "ignored: consumed or older event";
}
state.lastTime = signal.time;
state.last = { frame, signalTime: signal.time, direction: signal.value };
log?.("pine-signal-observer", state.last);
return signal.value === 1 ? "BUY observed" : "SELL observed";
}The two-minute freshness window is an example for short-interval experiments. It is measured from the plot's bar timestamp, which can be an opening boundary. Revisit it when changing interval or the expected consumption time.
What this observer establishes
A zero means no new directional event in the example indicator. The reader intentionally finds the most recent nonzero event within its allowed age, so a later zero does not cancel that event. A consumed event is not logged again, even if its value changes under the same timestamp.
Future points, old points, string directions, and malformed point shapes are rejected. The script makes no orders and does not implement portfolio or broker reconciliation. Its cached last record is historical diagnostic state, not a promise that the event remains fresh later.
Add execution separately
Before turning an observed event into a request, inspect pending orders, current position, action ownership, and execution controls. Keep the signal timestamp and consumption timestamp in metadata.
The Pine-to-SJS execution tutorial extends this numeric event contract with pending-order and position checks. It keeps signal-production time separate from the frame that consumes it.
Continue with strategy orders for the alternative Pine-owned request path.