Build

Writing strategies by hand

The workspace is a real IDE in your browser: a file tree, a Python editor with live intelligence, tests, snapshots, and run buttons. This guide explains the layout, what every file in a strategy workspace does, the contract your strategy code fulfills, the market data it can read, and the safety rails around all of it.

The workspace IDE: file tree, Python editor with diagnostics, and run controls
The workspace IDE — files on the left, editor in the middle, snapshot history or the AI builder on the right, and the panel strip along the bottom.

The workspace layout

  • Header — the workspace name, a storage-quota chip (“N% of quota”), a mode toggle between Code and Spec builder, the autosave chip, the run buttons (Run Tests, Preview, Backtest, Run Notebook), and Create strategy with AI.
  • Left — Files. The file tree, with New file, rename and delete. A dot marks files with unsaved changes.
  • Center — the editor, with tabs for each open file.
  • Right — the side panel, switching between the snapshot history timeline and the AI builder.
  • Bottom — the panel strip with five tabs: Problems (diagnostics from the editor and the import lint), Logs, Artifacts (files produced by runs), Tests (per-case results), and Outputs (notebook run output). All panes are drag-resizable.

What each file does

A scaffolded strategy workspace (whether created by the AI builder or from the template) contains a small, opinionated set of files. Keeping each concern in its own file is what makes strategies reviewable — you always know where to look.

FileWhat lives there
strategy.py The strategy class itself: its name and version, its parameter schema, the market-data features it needs, the instruments it supports, and the callback methods described below. Exactly one class in this file is the strategy.
features.py Which data features the strategy consumes, plus small helper functions that derive values from them (pure calculations, easy to unit-test).
signals.py Setup detection: the conditions that say “this is our pattern”, producing setups with explicit entry and stop levels.
entries.py How a detected setup becomes actual orders — built with the named order constructors (limit orders, stop orders, brackets).
exits.py Position management: for each closed bar, the complete set of working orders you want to exist (the app reconciles that with what is already working).
risk.py Sizing and pre-trade guards — max stop distance, per-trade risk sizing, force-flat times. Guards should reject loudly, not silently shrink.
chart_annotations.py What the preview chart draws for each setup — levels, markers, zones. This is your visual proof that the code interprets the market the way you think it does.
params.yaml Default parameter values (e.g. target_r: 2.0, stop_ticks: 8), validated against the schema declared in strategy.py.
tests/ The required test suite — see Testing your strategy. Write these first.
fixtures/ Small synthetic data windows the tests replay, so tests are fast and deterministic.
README.md / AGENTS.md The workspace overview and the behavioral contract — the same rules for you and for any AI assistant working in the workspace. The contract is also mirrored as CLAUDE.md and .github/copilot-instructions.md so Claude Code and VS Code Copilot pick it up automatically; AGENTS.md is the canonical copy.

The strategy contract, in plain terms

Every strategy answers the same three questions, each with its own method on the strategy class:

  1. “Is my pattern here?”detect_setups(...) looks at the market data available so far and returns any setups it finds, each with explicit entry and stop levels.
  2. “What orders does that mean?”generate_orders(...) turns a setup into order intents: which orders, at which prices, protected how.
  3. “How do I manage the position?”manage_position(...) runs on every closed bar while in a trade and returns the full set of working orders you want at that moment; the app reconciles your desired state with reality.

Three supporting methods round out the contract:

  • on_prepare(...) — one-time setup before the session starts.
  • chart_annotations(...) — the levels, markers and zones to draw on the preview chart for each setup.
  • debug_trace(...) — the decision-relevant values (including reject reasons) shown in the debug trace panel during previews.

Entry order types

Custom strategies enter with resting limit orders (join at your price and wait) or stop entries (enter on a break through a trigger level). Market-at-any-price entries are not supported. In the realistic backtest, a triggered stop entry fills slightly worse than its trigger price — conservative realism — while the teaching preview fills it exactly at the trigger.

Two rules keep results honest, and the platform enforces both:

  • Decisions happen on closed bars. Your callbacks fire when a bar completes — there is no peeking inside a forming bar.
  • No looking ahead. All data access is through a cursor that only sees the past. If code tries to read a future value, it doesn't get a wrong answer — it gets a loud LookaheadError. One of the required tests exists specifically to prove your strategy trips this alarm correctly.

The market data available to your code

Strategies declare which features they need and read them through context.features(...). The catalog covers, by family:

  • Market data — 5-second and 60-second OHLCV bars that also carry per-bar delta (buy minus sell volume), bid/ask volume and cumulative delta; individual trade events; best bid/offer quotes; contract definitions and tick math (tick size, tick value, point value); continuous-contract mapping and roll metadata.
  • Sessions — the session calendar (RTH/ETH boundaries, holidays, early closes), session windows, and trade cutoffs (no-new-trades and force-flat times).
  • Price levels — prior-day high/low/close, overnight high/low, opening range, running session high/low, session VWAP (RTH, ETH and anchored) with bands, and rolling ATR.
  • Market structure — swings, sweeps, reclaims, retests, breaks of structure, failed breakouts, range detection.
  • Order flow — aggressor delta, cumulative delta, delta by price, delta divergence, volume by price, RTH volume profile, value area (POC/VAH/VAL), TPO profile, and absorption/exhaustion/imbalance proxies.
  • Technical indicators — a library of around 48 indicators computed on your bars: moving averages (EMA, SMA, WMA, HMA, DEMA, KAMA, VIDYA), RSI, MACD, Stochastic, ADX and Aroon, Bollinger and Keltner bands, Donchian channels, CCI, ROC, CMO, OBV, KVO, Ichimoku, SuperTrend, Parabolic SAR, MFI, and more, plus pivot systems (floor, Camarilla, Woodie) — each with a documented warm-up, so a value never appears before the indicator is ready. See Indicators & features for the full list.
  • Risk & exit helpers — time filters, fixed-tick / structural / ATR stops, R-multiple and VWAP targets, time exits, partial exits, trailing stops, force-flat.

Every feature documents when its values become knowable, and the cursor enforces it — an opening-range value, for example, simply does not exist until the opening range has completed.

Worked examples

Four ready-to-read example strategies ship as starting templates. Each one is a complete, tested strategy class that adapts a well-known trading pattern to the platform's contract and feature catalog — copy one into a workspace and reshape it, or just read it to see how the pieces fit. Every example declares the features it needs, sizes trades through the same pre-trade risk gate, rejects loudly when a stop is too wide, and force-flats at the session close.

Moving-average cross

The classic fast/slow moving-average crossover. It reads two indicator series — a fast ema and a slow sma — detects the crossover from the last two closed bars, and enters in the cross's direction with a stop entry through the signal bar (enter on the break). Demonstrates: reading indicator features, detecting a state transition under the cursor (the "previous" value is genuinely the prior closed bar's), and a stop-entry bracket.

Features: bars_5s, ema, sma.

required_features = ["bars_5s", "ema", "sma", "session_calendar"]

def detect_setups(self, context):
    fast = context.features("ema").upto_now()
    slow = context.features("sma").upto_now()
    if len(fast) < 2 or len(slow) < 2:
        return []
    # A bullish cross: fast was at/below slow last bar, above it now.
    if fast[-2]["ema"] <= slow[-2]["sma"] and fast[-1]["ema"] > slow[-1]["sma"]:
        return [self._build_setup(Direction.LONG, ...)]  # buy-stop through the bar high
    ...

Order-flow imbalance

An aggressor-imbalance entry that joins one-sided flow. On each closed bar it measures the bar's own ask-vs-bid aggressor imbalance, confirms it with the session cumulative delta's slope and a recent large print, and joins the flow with a resting limit order. Demonstrates: combining several order-flow features into a single confluence, all read causally. (The same imbalance is also available pre-summarized as the imbalance_proxy and delta_by_price profile features.)

Features: bars_5s, cum_delta, big_trades.

required_features = ["bars_5s", "cum_delta", "big_trades", "session_calendar"]

bar = context.features("bars_5s").upto_now()[-1]
buy_imbalance = bar["ask_volume"] >= bar["bid_volume"] * params.min_imbalance
slope = cum[-1]["cum_delta"] - cum[-1 - lookback]["cum_delta"]  # delta trend
big = self._recent_big_trade(context)                          # a large print, same side
if buy_imbalance and slope >= 0 and big is not None and big["side"] == "B":
    # join the flow with a resting limit at the signal bar close
    return [self._build_setup(Direction.LONG, bar, slope, big, context)]

Indicator confluence

A "the stars line up" rule made explicit: three independent signals must agree before a trade is taken — price on the right side of the session VWAP (trend), RSI crossing through its oversold threshold (momentum), and a confirmed swing to anchor the stop (structure). Demonstrates: combining indicators from different families with market structure, and placing the stop at a real level so the market defines the risk.

Features: bars_5s, rsi, vwap_rth, swings.

required_features = ["bars_5s", "rsi", "vwap_rth", "swings", "session_calendar"]

if close > vwap and rsi[-2]["rsi"] <= level < rsi[-1]["rsi"]:  # trend + momentum turn
    anchor = self._nearest_swing(context, "low", below=close)   # structure stop
    if anchor is not None:
        stop = anchor["price"] - buffer_ticks * tick
        return [self._build_setup(Direction.LONG, ..., anchor, context)]

Value-area fade

A market-profile fade. It reads the prior session's value area (POC / VAH / VAL); when a bar pokes below the value-area low and closes back inside, it buys the rejection and targets the point of control (mirror at the value-area high). The value-area feature is delivered as a no-lookahead projection — one row per completed session — so "trade off the prior day's profile" is honest by construction. Demonstrates: a session-boundary feature and a structure-derived target rather than a fixed reward multiple.

Features: bars_5s, value_area.

required_features = ["bars_5s", "value_area", "session_calendar"]

va = context.features("value_area").at_or_before(context.now)  # PRIOR, closed session only
bar = context.features("bars_5s").upto_now()[-1]
if bar["low"] <= va["value_area_low"] <= bar["close"] < va["poc"]:
    # rejected the low edge back inside value -> fade up to the POC
    entry, target = va["value_area_low"], va["poc"]
    return [self._build_setup(Direction.LONG, bar, entry, target, va, context)]

Editor intelligence

Python files get real language support, running entirely in your browser tab:

  • Completions as you type, including the platform SDK's types and functions, with documentation attached.
  • Hover over any symbol for its type and docs.
  • Signature help when you open a call's parentheses.
  • Live diagnostics — syntax and type findings appear as squiggles in the editor and as rows in the Problems panel.

Language intelligence is deliberately best-effort: if it can't start in your browser session, the editor keeps working as a plain editor — it never blocks typing.

Import rules — what's available, what's blocked

Strategy and research code runs in an isolated environment with no network access and strict resource limits. Consistent with that, imports come from a fixed list:

  • Available: a broad, safe slice of the Python standard library (math, statistics, datetime, collections, dataclasses, itertools, json, re, zoneinfo and friends) plus numpy, pandas, pyarrow, and the platform's own strategy_sdk and platform_sdk.
  • Blocked: anything that reaches outside the isolated environment — networking (socket, requests, urllib…), process and shell control (subprocess, os, sys), direct filesystem access (pathlib, shutil, io), unsafe deserialization (pickle and friends), threading/multiprocessing, and low-level tricks (ctypes, dynamic imports).

Why you see a lint flag: the moment you type a blocked import, a warning appears in the editor and the Problems panel telling you the module won't be importable when the code runs — for example that network access is denied, or that file paths are handed to your code by the platform rather than opened directly. The flag is advisory (you can keep typing), but the isolated environment enforces the same rule at run time, so the lint is there to save you the round trip: fix it now rather than discover it when the run refuses the import.

Autosave, snapshots and restore

Autosave

The header chip tells you exactly where your edits stand: Saved, Unsaved changes…, Saving…, or Saving failed — retrying. If saving ever fails, a banner explains that your edits are kept locally and re-sent automatically — nothing is discarded, even if you close the tab.

Snapshots

The right rail's history timeline records the workspace over time. Snapshots are taken automatically (including before every run), and you can cut one yourself at any time: type an optional name into “Name this snapshot” and press Snapshot now. Each entry shows its kind (autosave, manual, pre_run, restore), a short content fingerprint, and a timestamp — and every run permanently records exactly which snapshot of your code it ran.

Diff and restore

Diff vs current shows a side-by-side comparison of any snapshot against the live tree, per file (changed / unchanged / only in snapshot / only in current). Restore rolls the live tree back — and here's the nice part: history is never rewritten. The restore itself is recorded as a new snapshot, so you can always roll forward again. Unsaved edits are flushed before the roll; no keystrokes are lost.