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.csorstrategy.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:
| Language | A helper | How the file that runs uses it |
|---|---|---|
| Python | ranges.py | import ranges or from ranges import highest |
| Rust | range.rs | mod range; |
| C# | Filters.cs | compiled 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.mdmain.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.mdParameters 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, andctx.legslists each signal strategy once per bar:
| Field | What it holds |
|---|---|
name | the file, such as signal_1.pine |
language | pine, python, rust, csharp or visual |
instrument | the instrument it runs on |
direction | "LONG", "SHORT", or None when it wants no position |
stop_pips, target_pips | the distances it asked for, None without one |
lots | the size it asked for, None when it leaves that to the handler |
profit_pips, trades | its 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 assignal_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(): eachLeghasname,language,instrument,direction(Some(Direction::Long),Some(Direction::Short)orNone),stop_pips,target_pips,lots,profit_pipsandtrades. Export it withexport!for one position or withexport_robot!for a position per signal strategy. An agent makes one withcreate_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_strategytakes the file that runs ascode, its helpers asfilesand its notes asnotes. 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_strategyadds or replaces code, helpers and notes; nothing is deleted.append_journaladds one dated entry tojournal.md.create_variantcopies a variant under a new name, with its own code or values;update_strategychanges a named variant, and makes one the default withdefault_variant.save_setupkeeps values for one instrument (and timeframe) inside a variant.create_strategywithhandler(kindandsignals) makes a handler workspace: language python (or rust for your own handler in Rust), the signal strategies files of it, andcodeleft out for a ready-made handler.update_strategywithhandler.signalsreplaces the list; a handler keeps its kind.read_strategyreturns 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_settingsreads Test's run options and what each allows for the strategy and the plan.run_backtestandrun_optimizationtake the same options by name, plusvariant, check them as the app does, and answer withsettings_used: every setting the run used, the variant and the setup, and the engine that ran.
The rules an agent follows:
- One workspace is one thing that runs. A new idea is a new workspace; several strategies that trade together are one handler workspace.
- The file that runs stays short; reused logic goes into helpers.
- Trying a change means a new variant (
create_variant), never overwriting the default. Make it the default only when the user says so. - Parameters for one pair go into that pair's setup (
save_setup), not into the variant's defaults. - Write
README.mdwith the workspace, and keep it current. - After every run, optimization or code change, call
append_journal. - Name the variant, the setup and the run options in every journal entry, and read
settings_usedinstead of assuming them. - Never delete or rewrite a note the user wrote.