Build an indicator-driven entry
Replace a local calculation with a registered SMA while keeping explicit entry guards.
Prerequisites
Use one instrument, one-minute historical input, no other order source, and initially no position. This entry-only example demonstrates a registered calculation; it does not supply an exit plan. Inspect it in a short simulated experiment.
Add the complete source
The script observes the relationship between current close and cal("sma", { minutes: 3 }). It requests one unit only after that relationship crosses upward and orders/positions are empty.
export async function app({ context, bar, cal, log }) {
const frame = bar?.date == null ? NaN : new Date(bar.date).getTime();
if (!Number.isFinite(frame) || typeof bar.close !== "number"
|| !Number.isFinite(bar.close)) return "waiting: invalid bar";
const state = (context.sjsState ??= {}).indicatorEntry ??= {};
if (state.busy) return "waiting: busy";
if (state.lastFrame != null && frame <= state.lastFrame) return "ignored: old frame";
if (state.lastFrame != null && frame - state.lastFrame > 120000) state.relation = undefined;
state.lastFrame = frame;
state.busy = true;
try {
const average = await cal("sma", { minutes: 3 });
if (typeof average !== "number" || !Number.isFinite(average)) {
state.relation = undefined;
return "waiting: SMA unavailable";
}
const previous = state.relation;
state.relation = Math.sign(bar.close - average);
if (!(previous != null && previous <= 0 && state.relation > 0)) return "hold: no upward crossing";
state.signal = { frame, close: bar.close, average };
if (typeof context.getOrders !== "function") return "waiting: orders unavailable";
const orders = await context.getOrders();
const positions = await context.getPositions();
if (!Array.isArray(orders) || !Array.isArray(positions)) return "waiting: invalid snapshot";
if (orders.length || positions.length) return "waiting: exposure exists";
state.lastAttempt = { frame, outcome: "pending" };
const result = await context.placeOrder({
id: `indicator-entry-${frame}`, instrument: context.instrument,
action: "BUY", quantity: 1, type: "MARKET",
}, { source: "sjs", metadata: { strategy: "indicator-entry", signal: state.signal } });
state.lastAttempt.outcome = result !== false && result != null ? "accepted" : "not-accepted";
log?.("indicator-entry", state.lastAttempt);
return state.lastAttempt.outcome;
} catch (error) {
state.relation = undefined;
state.lastError = String(error);
if (state.lastAttempt?.frame === frame) state.lastAttempt.outcome = "error";
log?.("indicator-entry-error", state.lastError);
return "error";
} finally {
state.busy = false;
}
}Verify the result
With calculator values fixed at 100, closes 99, 101 on increasing frames should produce one entry attempt. The first observation only establishes the relation. A missing, string, or non-finite calculation clears the relation; the next valid value establishes it again.
Check indicatorEntry.signal and lastAttempt, then the actual order and position. A pending order or any existing position prevents entry. A blocked crossing is consumed, not retried merely because price stays above the average.
Understand the data change
This SMA requests a historical time window and averages returned closes. It is not guaranteed to equal the first tutorial's last three observed closes or to contain exactly three bars. A finite value does not prove complete history; verify coverage before making readiness part of a decision.
Common mistakes are treating undefined as zero, calling every above-average frame a crossover, and assuming this entry-only walkthrough manages a position afterward.
Next: prevent duplicate orders.