Your first SJS strategy
Run a standalone moving-average crossover with explicit state, order checks, and execution outcomes.
This example buys one unit when the current close crosses above the mean of the last three observed closes, then requests a close when it crosses below. Equality counts as neither above nor below. The example is long-only and assumes one instrument, at most one position, and no other order-producing code in the session.
It is an execution walkthrough, not a claim about the strategy's performance.
1. Prepare a historical experiment
Choose an instrument with available one-minute historical bars and a short test range. Leave Pine, L3, and other order-producing models disabled for this first run. Start a fresh session when changing the example's settings.
The strategy processes the first valid observation at each increasing timestamp. Use a bar-based historical run for this walkthrough: the code does not determine whether a live updating bar has closed. Its two-minute gap limit assumes one-minute input; a longer gap restarts signal warmup.
2. Paste this into SJS
This is the complete source. It requires no runtime imports, provider credentials, or configured indicator models.
const settings = { period: 3, quantity: 1, maximumGapMs: 120000 };
export async function app({ context, bar, log, skipall }) {
if (bar?.date == null || typeof bar.close !== "number"
|| !Number.isFinite(bar.close)) return "waiting: invalid bar";
const frame = new Date(bar.date).getTime();
if (!Number.isFinite(frame)) return "waiting: invalid timestamp";
const root = context.sjsState ??= {};
const state = root.crossoverTutorial ??= { closes: [] };
if (state.busy) return "waiting: evaluation in progress";
if (state.lastFrame != null && frame <= state.lastFrame) {
return "ignored: repeated or older timestamp";
}
if (state.lastFrame != null && frame - state.lastFrame > settings.maximumGapMs) {
state.closes = [];
state.previousRelation = undefined;
}
state.lastFrame = frame;
state.signal = undefined;
state.closes = [...state.closes, bar.close].slice(-settings.period);
if (state.closes.length < settings.period) return "waiting: warmup";
const average = state.closes.reduce((sum, close) => sum + close, 0) / settings.period;
const relation = Math.sign(bar.close - average);
const previousRelation = state.previousRelation;
state.previousRelation = relation;
const side = previousRelation != null && previousRelation <= 0 && relation > 0
? "BUY"
: previousRelation != null && previousRelation >= 0 && relation < 0
? "EXIT"
: "HOLD";
state.signal = { frame, side, close: bar.close, average };
if (side === "HOLD") return "hold: no crossover";
state.busy = true;
try {
if (typeof context.getOrders !== "function") {
return "waiting: order inspection unavailable";
}
const orders = await context.getOrders();
const positions = await context.getPositions();
if (!Array.isArray(orders) || !Array.isArray(positions)) {
return "waiting: invalid execution snapshot";
}
if (orders.length) return "waiting: existing orders";
if (positions.length > 1) return "waiting: multiple positions";
const position = positions[0];
if (position && position.action !== "BUY") return "hold: position is not long";
if (side === "BUY" && position) return "hold: already long";
if (side === "EXIT" && !position) return "hold: already flat";
const metadata = { strategy: "crossover-tutorial", signal: state.signal };
state.lastAttempt = { frame, side, outcome: "pending" };
const result = side === "BUY"
? await context.placeOrder({
id: `crossover-entry-${frame}`,
instrument: context.instrument,
action: "BUY",
quantity: settings.quantity,
type: "MARKET",
}, { source: "sjs", metadata })
: await context.closePositions(position, { source: "sjs", metadata });
const accepted = result !== false && result !== undefined && result !== null;
state.lastAttempt.outcome = accepted ? "accepted" : "not-accepted";
log?.("sjs-crossover", { ...state.signal, ...state.lastAttempt });
if (accepted) skipall();
return accepted ? "execution: accepted" : "execution: not accepted";
} catch (error) {
state.lastError = String(error);
if (state.lastAttempt?.frame === frame) state.lastAttempt.outcome = "error";
log?.("sjs-crossover-error", { frame, error: state.lastError });
return "execution: error";
} finally {
state.busy = false;
}
}3. Understand the sequence
The first two valid timestamps warm up the sample window. The third establishes the first price/average relation. Later timestamps can produce a crossover. The signal uses only observations already received; the current observation is included in its mean.
Before requesting execution, the strategy inspects orders and positions. Any existing order makes this example wait, including an exit order. It does not cancel orders, add to a position, handle short positions, or manage multiple positions.
The timestamp is consumed before asynchronous reads or submission. A crossover blocked by existing orders, rejected by execution, or interrupted by an error is not retried on that timestamp or merely because the price stays above/below the mean. A later crossover is a new opportunity.
4. Inspect decisions and outcomes
Inspect context.sjsState.crossoverTutorial.signal, lastAttempt, and lastError when available in your session tooling. Submission attempts appear in logs under sjs-crossover; thrown failures use sjs-crossover-error. The returned strings describe early exits, but the shared runner's logging configuration determines whether those strings are visible.
An accepted result means the session API returned something other than false, undefined, or null. It is not confirmation of a fill. Compare the session's orders, trades, and position state.
For a fixed example, closes 100, 100, 100, 103, 99 at one-minute intervals produce a BUY crossover on the fourth observation and an EXIT crossover on the fifth. The close request on the fifth also requires the entry to have filled and no order to remain pending.
Continue with Lifecycle, or inspect the worked examples.