Authoring a strategy¶
This page wires a .strategy file into a running engine. It covers where files go,
how the three-way name match works, the CLI validation commands, and how to verify the
engine loaded the right file. For the script language itself, see
Strategy script language.
A strategy change is not hot-reloaded
The engine reads, validates, and compiles every strategy file only at startup.
A new or edited strategy takes effect on the next full start
(docker compose down && docker compose up -d). There is no reload command.
1. How binding works¶
An asset group that runs a primary strategy names it in strategy.name. The
legacy flat trading array uses strategy for the same value:
{ "strategy": { "name": "jacobs_ladder" } }
The engine resolves it in three steps, and all three must agree:
| Step | What the engine does | Example |
|---|---|---|
| 1. Config key | reads strategy.name from the asset group (strategy in the flat layout) |
"jacobs_ladder" |
| 2. File lookup | tries strategies/<key>.strategy, then strategies/<key>.json (relative to the engine's working directory, which is /app in the container) |
strategies/jacobs_ladder.strategy |
| 3. Header match | requires strategy <name> inside the file to equal the config key (dots normalise to underscores) |
strategy jacobs_ladder |
A mismatch at any step is a fatal error at startup.
A second strategy file can attach to an existing asset group as an overlay. It uses the
same loader, validation, and name match, but it is bound from the group's overlay section
rather than a second strategy, so it runs inside the same asset domain. See
Overlay entries.
2. Prerequisites¶
- A working deployment with
config.json,.env, andcompose.yaml(Deploying). dockeranddocker compose(or a localdiobuild for development).- The engine must be restarted to pick up a strategy change.
3. Write the file¶
Create a file at strategies/<your_name>.strategy. Use dots between words; dots
become underscores internally, so my.grid becomes the id my_grid.
There are two paths, and they are not the same size of job:
- A small order overlay. Start with Your first
strategy. It builds a working overlay step by step. The generic
order.place,order.replace, andorder.cancelactions are the ones wired for this. - A primary strategy. Copy
strategies/jacobs_ladder.strategyand edit it. A primary strategy owns the asset's whole buy/sell cycle, so it composes the grid lifecycle actions. The Action glossary explains what each phase does.
The older place.buy / place.sell examples you may find in old notes are reserved
names. They validate, but the current runtime does nothing with them. Use order.place
or order.replace instead.
See Strategy script language for the full language (remembers, tunable,
triggers, steps, guards, and the action vocabulary).
4. Place the file¶
Container (published image)¶
The image bakes the repo's strategies/ directory at /app/strategies and runs the
engine with cwd /app. Because the container is read-only and runs as a non-root user,
you cannot write to that directory at runtime.
Option A: bind-mount (no image rebuild)
Keep a strategies/ directory next to config.json and mount it read-only:
services:
engine:
volumes:
- ./strategies:/app/strategies:ro
The mount replaces the baked directory
Bind-mounting hides the image's strategies/ entirely. Copy any shipped strategy
you still reference into your host strategies/:
mkdir -p strategies
docker run --rm -v "$PWD/strategies:/out" --entrypoint cp \
ghcr.io/malciller/dio-domains:latest \
/app/strategies/jacobs_ladder.strategy /out/
Option B: rebuild from source
Place your file in the repo's strategies/ directory and rebuild the image.
The Dockerfile copies strategies/ into the image:
docker build -t dio .
Local development (no container)¶
Run dio from the repo root so the relative strategies/<name>.strategy resolves:
dune build
./_build/default/bin/main.exe
5. Validate¶
Validate before deploying. It catches typos, wrong action names, bad references, and duplicate labels.
With the published image:
docker run --rm \
-v "$PWD/strategies:/work/strategies" \
-w /work \
--entrypoint dio \
ghcr.io/malciller/dio-domains:latest \
strategy validate strategies/my.grid.strategy
With a local build:
dune exec dio -- strategy validate strategies/my.grid.strategy
Expected output on success:
strategy "my_grid" (strategies/my.grid.strategy): ok
On failure, diagnostics print with error / warning prefixes and the command exits 1:
strategy "my_grid" (strategies/my.grid.strategy): 1 error(s), 0 warning(s)
error strategies/my.grid.strategy: unknown action "place_bet"
Validate accepts both .strategy and .json files.
Compile (optional)¶
Lower the script to the engine's internal JSON form for inspection or diffing:
docker run --rm \
-v "$PWD/strategies:/work/strategies" \
-w /work \
--entrypoint dio \
ghcr.io/malciller/dio-domains:latest \
strategy compile strategies/my.grid.strategy -o strategies/my.grid.json
The engine prefers .strategy over .json when both exist, so compiling is not
required. It is for inspection, diffing, and trace-based replay.
6. Bind in config.json¶
Add or update the asset group with your strategy name:
{
"trading": {
"assets": [
{
"asset": "BTC",
"strategy": {
"exchange": "hyperliquid",
"symbol": "BTC/USDC",
"name": "my_grid",
"qty": "0.0001",
"grid_interval": [0.1, 0.5],
"testnet": true
}
}
]
}
}
The strategy.name value must match the name in your file's header. The engine is
strict: any misspelled or unknown key in config.json causes an immediate exit 1.
Overlays¶
To attach a second strategy file to an existing asset without creating another domain, add
an overlay section to that asset group:
{
"asset": "BTC",
"strategy": {
"exchange": "hyperliquid",
"symbol": "BTC/USDC",
"name": "my_grid",
"qty": "0.0001"
},
"overlay": { "exchange": "hyperliquid", "name": "<overlay-name>" }
}
overlay.exchange is the venue the overlay trades; overlay.symbol is optional and
defaults to the owning symbol when the venues match. The overlay file is validated exactly
like a primary strategy and must declare the same name. The primary strategy remains the
one bound to the domain; the overlay is an additional behaviour layer. No second domain,
feed subscription, or state key is created.
In the legacy flat layout the binding is an overlay object inside the trading entry,
{ "strategy": "<overlay-name>" }, and it inherits that entry's exchange and symbol.
An asset group has at most one overlay; don't stack two. For a custom overlay, leave the
carry params out and put the defaults in the file's tunable: block. The built-in carry
params are specific to the carry overlay, not a general parameter channel. Your first
strategy walks through the smallest working overlay.
7. Restart and verify¶
docker compose down
docker compose up -d
docker compose logs -f engine
On success¶
The engine logs the exact file it loaded:
strategy: BTC/USDC running strategies/my.grid.strategy
On failure¶
The engine exits immediately with one of these messages:
| Log message | Cause |
|---|---|
Strategy file for EXCHANGE/SYMBOL does not exist (tried strategies/<name>.strategy and strategies/<name>.json). |
The file is missing or the name doesn't match any file on disk. |
Strategy file 'PATH' for EXCHANGE/SYMBOL is invalid: PARSE_ERROR |
The file does not parse. Run dio strategy validate locally to get detailed diagnostics. |
Strategy name mismatch for EXCHANGE/SYMBOL: config.json declares 'X' but strategy file 'PATH' declares 'Y' |
The header in the file disagrees with the config value (after dot-to-underscore normalisation). |
In every case the engine refuses to start.
8. Iterate safely¶
- Edit the
.strategyfile. - Run
dio strategy validate ...(exit 0 is good). - Optionally compile and diff (
dio strategy compile ...;dio strategy diff a.json b.json). - Restart the engine (
docker compose down && docker compose up -d). - Check logs for the success line.
Trace-based regression
Set "strategy_trace": true at the top level of config.json to record per-cycle
market data. Use dio strategy replay and dio strategy diff to prove a revised
strategy reproduces the same order intents against a recorded trace before deploying.
9. Tuning and persistence¶
tunable parameters¶
The tunable: block declares default values in the file itself. Primary strategies have no
per-instance override key in config.json; changing defaults means editing the strategy
file and restarting the engine. The built-in carry overlay is the exception: its overlay
entry supplies a fixed set of params keys. A custom overlay should use its own file
defaults and leave params out.
Persisted state¶
Variables declared with persist inside remembers survive restarts. State is keyed
by {strategy}:{symbol}:{venue} in the data directory (/app/data):
| File | Contents |
|---|---|
accumulation_state.json |
Accumulated base quantities and P&L bookkeeping |
sell_levels_state.json |
Pending sell-level orders |
Changing the strategy name (or the types/meaning of a persisted remembers variable)
effectively starts with a clean slate in the old file's data. To reset cleanly:
docker compose down -v # deletes the persistent volume
docker compose up -d
10. Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
Engine exits immediately; log says does not exist ... refusing to start |
File missing, wrong directory, or the strategy name in config.json doesn't match any file stem. |
Ensure strategies/<name>.strategy exists in the engine cwd and the name matches. |
Engine exits; log says is invalid: ... |
Parse error in the .strategy file. |
Run dio strategy validate ... locally for line-level diagnostics. |
Engine exits; log says name mismatch |
The strategy <name> header in the file disagrees with the config.json value. |
Fix the header or the config value to match (dots normalise to underscores). |
dio strategy validate prints unknown action "..." |
Typo, or the action is not in the engine's built-in registry. | Check the spelling against the Action glossary or STRATEGY.md. New actions require an OCaml change by an engineer. |
| Engine starts but the old strategy still runs | Stale .json compiled artifact sitting alongside the .strategy file, or the .strategy was not included in a bind-mount. |
Delete the stale .json file or ensure the .strategy file is present; the engine prefers .strategy over .json. |