Workspaces

DocumentationIntroduction

Workspaces

A strategy is a workspace: the one file that runs, the helpers it uses, and a notebook. This page is the rulebook for building one, by hand in Build or through an AI agent over MCP. Agents: read it before you create a strategy.

One workspace, one thing that runs

  • A workspace runs exactly one file, named by its language: main.py, strategy.pine, Strategy.cs or strategy.rs. Build lists it under Runs, and Test names it in the card above the parameters.
  • A new idea is a new workspace, never a second strategy file in this one.
  • Every run records the file that ran, so two runs never mix strategies.

Helpers

Keep the file that runs short. Logic it reuses goes into helpers in its own language, imported by name:

LanguageA helperHow the file that runs uses it
Pythonranges.pyimport ranges or from ranges import highest
Rustrange.rsmod range;
C#Filters.cscompiled with the strategy as one project

A file in another language belongs in the workspace only when the file that runs reads a value from it, such as ctx.read("trend.pine", "up"). It then runs beside it on every bar.

Before a run starts, every helper is looked up: the Python imports of the file that runs and of each helper, Rust mod lines, and values read from other files. A missing one stops the run before any data loads, and the message names the file and the line: "main.py line 2 needs sessions.py, and the workspace has no such file." Test shows the same message in its card before you press Start.

Example: one strategy with helpers

Range breakout             Python
  main.py                  runs
  averages.py              helper   (from averages import sma)
  ranges.py                helper   (import ranges)
  notes: README.md, journal.md

main.py:

pythonfrom averages import sma
import ranges


class RangeBreakout(Strategy):
    def __init__(self, params):
        self.lookback = int(params.get("lookback", 20))
        self.trend = int(params.get("trend", 100))
        self.sl_pips = float(params.get("slPips", 25))
        self.tp_pips = float(params.get("tpPips", 50))

    def on_bar(self, ctx):
        bars = ctx.bars(self.trend + 1)
        if len(bars) < self.trend + 1:
            return
        closes = [b.close for b in bars]
        top = ranges.highest([b.high for b in bars[:-1]], self.lookback)
        bottom = ranges.lowest([b.low for b in bars[:-1]], self.lookback)
        up = closes[-1] > sma(closes, self.trend)
        if up and closes[-1] > top:
            ctx.signal("LONG", sl_pips=self.sl_pips, tp_pips=self.tp_pips)
        elif not up and closes[-1] < bottom:
            ctx.signal("SHORT", sl_pips=self.sl_pips, tp_pips=self.tp_pips)

averages.py and ranges.py:

pythondef sma(values, length):
    window = values[-length:]
    return sum(window) / len(window)
pythondef highest(values, length):
    return max(values[-length:])


def lowest(values, length):
    return min(values[-length:])

Variants and setups

A variant is a named version of the file that runs: its own code, its own parameter values and its own setups. The helpers and the notebook belong to the workspace and are shared by every variant. A workspace starts with one variant, v1, its default; a run that names no variant takes the default. At most 12 variants.

  • Trying a change means a new variant, copied from the current one, never overwriting the default.
  • The default changes only when you decide it does.
  • Every run records the variant that ran, so runs of two versions never mix.

A setup is a set of parameter values for one instrument, optionally one timeframe, inside a variant. A run of that instrument takes the setup by itself: one for the instrument and the timeframe first, else one for the instrument. Values the run sets itself sit above the setup's. At most 50 setups per variant.

ORB index open             Rust
  strategy.rs              runs
  range.rs                 helper   (mod range;)
  variants: v1 "London open" (default), v2 "NY open"
  setups of v2: NAS100 M15, US30 M15, GER40 M15
  notes: README.md, journal.md, research.md

Parameters that suit one pair go into that pair's setup, for example after an optimization on EURGBP, not into the variant's defaults.

In the app:

  • Build: the variant beside the workspace name opens its code in the editor; Save writes to that variant. Its menu makes a new variant from the open one, makes the open one the default, renames it or deletes it. The file column lists the open variant's setups under Setups.
  • Test: the chooser lists a workspace's variants under its row, and the chip names the one picked. The card above the parameters says the variant and which setup applies, or Default values. When you change the market, its setup loads by itself. A value changed by hand shows changed with Save to setup.
  • Optimize: Save to setup beside Apply to Backtest keeps the chosen pass's values for the sweep's instrument.
  • Runs are titled with the variant and the setup: "ORB index open · v2 · Setup US30 M15".

Handlers: several strategies trading together

A handler workspace runs one handler over signal strategies: files of the workspace, in any language a strategy is written in. Each signal strategy runs on a paper account of its own, on the run's instrument and timeframe, and never trades the real account. The handler reads what each one wants and places the real orders.

Pine trio                  Handler · Equal split
  main.py                  runs     (the handler)
  signal_1.pine            signal strategy
  signal_2.pine            signal strategy
  signal_3.pine            signal strategy
  notes: README.md, journal.md
  • New handler in Build's New menu makes one: Equal split, Majority vote or Own handler in Python, with one Pine signal strategy to start from. Add the others as files; a file's menu makes it a signal strategy or no longer one, and a file that is no longer one stays in the workspace without running. A handler keeps one signal strategy at least.
  • Equal split ships with the engine: every signal strategy that wants a position gets an equal share of Total lots, with its own stop and target, one position per signal strategy.
  • Majority vote ships with the engine: one position the way at least Signal strategies that must agree want, none on a tie, with the stop and target you set.
  • A ready-made handler's file is a note of what runs, not code. Your own handler is a Python class deriving from Handler, and ctx.legs lists each signal strategy once per bar:
FieldWhat it holds
namethe file, such as signal_1.pine
languagepine, python, rust, csharp or visual
instrumentthe instrument it runs on
direction"LONG", "SHORT", or None when it wants no position
stop_pips, target_pipsthe distances it asked for, None without one
lotsthe size it asked for, None when it leaves that to the handler
profit_pips, tradesits paper account's result so far
pythonclass EqualSplit(Handler):
    hedging = True

    def __init__(self, params):
        self.lots = float(params.get("lots", 0.30))

    def on_bar(self, ctx):
        if not ctx.legs:
            return
        share = round(self.lots / len(ctx.legs), 2)
        for leg in ctx.legs:
            mine = [p for p in ctx.positions if p.label == leg.name]
            if mine and leg.direction != mine[0].direction:
                for p in mine:
                    ctx.close_position(p.id)
                mine = []
            if leg.direction and not mine:
                ctx.open_position(leg.direction, share, sl_pips=leg.stop_pips,
                                  tp_pips=leg.target_pips, label=leg.name)
  • Parameters: Test lists the handler's own first, then each signal strategy's under its file. A value is named file::name, such as signal_1.pine::fast, and setups hold them like any other.
  • Validate in Build checks the handler and every signal strategy on its own.
  • A handler in Rust reads the same through ctx.legs(): each Leg has name, language, instrument, direction (Some(Direction::Long), Some(Direction::Short) or None), stop_pips, target_pips, lots, profit_pips and trades. Export it with export! for one position or with export_robot! for a position per signal strategy. An agent makes one with create_strategy, language rust and handler kind code.
  • The engine: Automatic runs Equal split, Majority vote or a Rust handler over Pine, Blocks and Rust signal strategies on the Rust core, with the same trades as on the Python engine; a handler in Python, or any signal strategy in Python or C#, keeps the run on the Python engine.
  • On the server a handler runs as it does here. Its Python files run in the strategy container, apart from the data and the engine; the rest runs on the Rust core or the Python engine, as Automatic picks. A server backtest of it is verified when every file that runs is isolated or runs on the core; the two ready-made handlers are the engine's own code.
  • Variants: a handler workspace has variants like any workspace. Each has its own handler code and values; the signal strategies and the helpers are shared, and setups follow the instrument.
  • A signal strategy says long, short or flat: one that opens several positions at once, places a resting order or closes part of a position is refused with its file.

The notebook

Notes are Markdown files beside the code. They never run. Keep three:

  • README.md: what the strategy is, its rules and its instruments. Write it when you make the workspace, and update it when the rules change.
  • journal.md: append only. One dated entry after every run, optimization or code change: what was tried, the run id, the result in one line and the next step.
  • research.md: sources and ideas, when there are any.

Never delete or rewrite a note somebody else wrote.

Run settings

Test's run options hold how a run is made, not what the strategy does: the market and the period, the starting balance and account currency, leverage, the lot size, commission, swap, the fill priority, the accuracy tier, the slippage at the tick tier, and the engine. The engine is Automatic by default: the Rust core for Pine, Blocks and Rust wherever it can run them, the Python engine for everything else. There is no spread setting: a run reads the data's own bid and ask.

For AI agents (MCP)

The MCP server of this app gives an agent the same workspace:

  • create_strategy takes the file that runs as code, its helpers as files and its notes as notes. A missing helper is refused with its file and line; a file in another language that nothing reads is refused, because a second strategy is its own workspace.
  • update_strategy adds or replaces code, helpers and notes; nothing is deleted.
  • append_journal adds one dated entry to journal.md.
  • create_variant copies a variant under a new name, with its own code or values; update_strategy changes a named variant, and makes one the default with default_variant. save_setup keeps values for one instrument (and timeframe) inside a variant.
  • create_strategy with handler (kind and signals) makes a handler workspace: language python (or rust for your own handler in Rust), the signal strategies files of it, and code left out for a ready-made handler. update_strategy with handler.signals replaces the list; a handler keeps its kind.
  • read_strategy returns the layout: the file that runs, its helpers, its notes and its variants, each with its setups, and a handler's signal strategies.
  • get_run_settings reads Test's run options and what each allows for the strategy and the plan. run_backtest and run_optimization take the same options by name, plus variant, check them as the app does, and answer with settings_used: every setting the run used, the variant and the setup, and the engine that ran.

The rules an agent follows:

  1. One workspace is one thing that runs. A new idea is a new workspace; several strategies that trade together are one handler workspace.
  2. The file that runs stays short; reused logic goes into helpers.
  3. Trying a change means a new variant (create_variant), never overwriting the default. Make it the default only when the user says so.
  4. Parameters for one pair go into that pair's setup (save_setup), not into the variant's defaults.
  5. Write README.md with the workspace, and keep it current.
  6. After every run, optimization or code change, call append_journal.
  7. Name the variant, the setup and the run options in every journal entry, and read settings_used instead of assuming them.
  8. Never delete or rewrite a note the user wrote.
Orion Quant Technologies