Practical tutorials

Build a session-aware strategy

Apply a local-time entry window while preserving state updates and exits.

Prerequisites

Use the first crossover tutorial with one-minute historical bars, one instrument, and no other order source. This version permits entries from 09:30 inclusive to 10:00 exclusive in America/New_York. It is a clock filter, not an exchange calendar.

Add the complete source

The filter is applied only to BUY decisions, after signal state and execution snapshots are checked. Exit decisions remain available outside the entry window.

const settings = { period: 3, quantity: 1, maximumGapMs: 120000 };

export async function app({ context, bar, log, skipall, timeFilter }) {
  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";

    if (side === "BUY" && !timeFilter(bar, "09:30", "10:00", {
      timezone: "America/New_York", includeEnd: false,
    })) return "hold: outside entry window";

    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;
  }
}

Verify the boundary

Using a fixed winter date, compare an upward crossing at 09:30 New York time with one at 10:00. The former may request entry; the latter is blocked. Establish a long position and a later downward crossing outside the entry window: it should still request a close when no order is pending.

Check crossoverTutorial.signal even when entry is blocked. Putting the filter at the top of the function would also suppress state updates and exits, which is a different strategy.

Account for calendar and input gaps

Timezone conversion handles the selected local clock, but this filter does not independently exclude holidays or weekends. Historical session filtering and data availability remain separate. The crossover's two-minute gap reset still applies across interruptions.

A crossing outside the entry window is consumed. Opening the window later does not automatically retry that earlier crossing. Common mistakes are comparing UTC bounds with local clock labels or changing the input interval without changing the gap policy.

Next: stateful exits.

On this page