Strategy Workspace and Backtesting¶
The Strategy Workbench is a three-section dashboard surface — Strategies (project/revision manager), Studio (code editor), and Backtests (submission and results) — plus a right-zone Sandbox panel. All of it reads and writes the backend's spine.v2 Strategy Studio contract (onix.strategy_*, onix.backtest_* schemas vendored under contracts/vendor/onix-engine/schemas/), and none of it computes a trading result itself.
Authoring surface¶
The Studio (src/strategy/studio/studio.tsx) is a CodeMirror-based Python editor (@codemirror/state, @codemirror/lang-python, @codemirror/merge for diffing against a revision) built as an editor-first layout: the code surface occupies most of the widget, with a collapsible Problems/Validation/Output/Backtest/History drawer and a hidden-by-default file inspector. It edits a strategy's mutable draft tree, or displays one immutable revision addressed by its 64-character content hash — never both conflated; isDraftTarget/StudioTarget in src/strategy/contract.ts is the type-level guard for that distinction.
Editor intelligence — completions, hover text, and the vocabulary of diagnostic codes a validation can return — comes exclusively from the backend's own GET /v2/authoring/registry document (onix.strategy_authoring_registry), read by src/strategy/authoring/registry.ts. That module's own "hard rule" is explicit: it "NEVER hard-codes, bundles, or invents a Nautilus class/method name, a completion item, or hover/diagnostic CONTENT to make the editor look more capable than the backend has actually published." Before this document existed, the same surface could only prove a route had been declared, never hold real completion content, and rendered UNAVAILABLE for every build — the module's comment records this history plainly rather than hiding it.
A strategy's draft is validated against the installed Nautilus pin via POST /v2/strategies/{id}/validate, which returns a typed ValidationReceipt with per-diagnostic severity; a passing receipt is what gates whether the current draft content may be saved as a new immutable revision.
Backtest submission: what the wire actually accepts¶
The run-config panel (src/strategy/runs/run-config.tsx) lets an operator pick a saved, content-hash-addressed revision, one or more instruments, a resolution, and a nanosecond time window, then submits via POST /v2/backtests. The panel's own documentation states four hard facts about the wire contract it is bound to, worth restating because they shape what the UI can and cannot offer:
- One instrument per job.
instrument_idon the request is a single optional string, not an array — a multi-instrument run is N separate backend jobs, submitted as a browser-local batch (submitBacktestBatch). The UI states this grouping is local bookkeeping, never a backend fact, and no synthetic combined-portfolio result is ever derived from the legs. bar_typeis composed, never freely typed — frominstrument_idplus the exactbar_type_suffix/v2/resolutionspublishes for the chosen resolution, the same construction the datafeed uses (see chart-datafeed.md).- Run mode is explicit. Every submission sends
run_mode(ONE_SHOTby default,STREAMINGas an explicit operator choice, with a boundedchunk_sizeunderSTREAMINGonly) rather than relying on an ambient backend default. - Some controls are shown but not submittable. Initial capital, account currency, fee model, fill model, and a deterministic seed have no field on this submit shape at all — the backend rejects the entire request with a named HTTP 400 error when an unknown field is present, so the UI does not send them. They render as explicitly inert rows naming the reason and pointing at the project's own open contract request (
docs/contract-requests/026-backtest-run-controls.md) rather than being hidden. Once a job finishes, the same five facts come back on the job detail'sexecutionblock and are rendered verbatim — "typed on the answer, refused on the request" is the code's own summary of this asymmetry.
Results: rendered verbatim, with an honest gap on progress¶
The results view (src/strategy/runs/backtests.tsx) renders a job's own published JobStatus vocabulary (seven values: QUEUED, RUNNING, and five terminal states) with a one-line explanation per status, never a renamed or invented label; an unrecognized future status renders as-is and is treated as still active. The code is explicit about a real limitation rather than working around it cosmetically: BacktestJobEvent "carries only STATE and LOG," and the job document itself has no percentage, bar counter, or ETA — so the widget renders no progress bar and no time estimate, because there is no published fact to draw one from.
Orders, fills, and positions tables read the backend's paginated GET /v2/backtests/{job_id}/report route and fall back to a DONE job's inlined reports block when the paginated route is not the active source; every table's caption states which of the two sources is on screen and whether more pages remain. A stats block is an open, untyped object on the wire; the UI matches it against a small set of self-checking known shapes or declines the block entirely — "never partially matched, never coerced" — falling back to a raw verbatim view underneath that "cannot be wrong." Where the backend publishes a sparse returns series, the UI draws only the published points, with nothing interpolated, zero-filled, or compounded into an equity curve.
Sandbox: what "live" means here, and what it does not claim¶
The Sandbox panel (src/strategy/runs/sandbox-panel.tsx) starts and stops a spine.v2 sandbox run against a saved revision (POST /v2/sandbox/start, /stop) and polls its status. The panel's own comment calls this "the live spine.v2 sandbox" — "live" there describes real-time execution as opposed to a finite backtest replay, not a claim about real-money settlement. The panel is explicit that it duplicates nothing: the account/orders/fills/positions widgets already poll the same live surface a running sandbox populates, so this panel states that plainly rather than re-rendering them.
This is frontend-only wiring, not a claim that the sandbox lane currently works end to end. At the reviewed backend pin, sandbox_start, sandbox_stop, and sandbox_status are each implemented as an explicit deferred/unavailable result server-side — the same deferred posture the backend's capability register documents for the core spine's account/portfolio/order surfaces this panel's sibling widgets read. A submission from this panel therefore reaches a real, but not-yet-implemented, backend route at this pin, not a functioning sandbox.
Two structural facts bound what this surface can mean in practice, and this document deliberately makes no stronger claim than the code supports:
- The entire mutation path — starting a strategy, starting a sandbox, submitting a backtest, saving a revision — is gated behind
PrivateOperatorSurface/useMutationsAllowed, which resolves tofalseunless the host (not the browser, not any user credential) is configured asprivate-operator. A public deployment relays none of these routes at all — see gateway-realtime.md. - Whether a given connected account is
paper,live, orsandboxis a string the backend publishes on its connection document and the frontend renders verbatim in the sharedConnectionSelectionContext(dashboard-and-workspaces.md); the frontend does not compute, default, or infer this label. (unverified at this pin: whether a "live"-labelled connection can route a sandbox run to real-money broker execution is a backend/venue fact, not something observable from the frontend source alone.)
This document does not claim real-money execution as a frontend capability, and no code path reviewed at this pin constructs an order intended to bypass that connection-environment label.
Status¶
Revision-based submission, the four wire-shape constraints above, verbatim results rendering, and the private-operator gate on every mutation are verified against source and unit tests (run-config.test.tsx, backtests.test.tsx, sandbox-panel.test.tsx, f9-acceptance.test.tsx). The five execution-control fields absent from the submit contract (capital, currency, fees, fill model, seed) are target-contract — requested, not yet accepted — per docs/contract-requests/026-backtest-run-controls.md. The sandbox surface's backend deferred status is checked directly against the reviewed backend pin (python/src/onix_engine/spine/service.py's sandbox_start/sandbox_stop/sandbox_status each raising a deferred result), not inferred from frontend behavior alone. See ../backend/trading/backtesting.md for the backend's own account of backtest execution and Nautilus's role in it, and capability-status.md for the consolidated table.
Evidence and source pins for this page
Verified. Current behaviour, confirmed in source at the pinned commit.
Verified on against the following immutable sources:
frontend@99f49dae:src/strategy/contract.tsfrontend@99f49dae:src/strategy/runs/backtests.tsxfrontend@99f49dae:src/strategy/runs/run-config.tsxfrontend@99f49dae:src/strategy/runs/sandbox-panel.tsxfrontend@99f49dae:src/strategy/studio/studio.tsxfrontend@99f49dae:src/gateway/deployment-mode.tsbackend@efa38e04:python/src/onix_engine/spine/service.py
Status tokens are defined on the documentation and status model page. Every pin on this site is listed under versions and source pins.