Setting up 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 scripts.
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¶
Each entry in the "trading" array in config.json names a strategy:
{ "strategy": "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" from the trading entry |
"jacobs_ladder" |
| 2. File lookup | tries strategies/<key>.strategy, then strategies/<key>.json (relative to the engine's working directory — /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.
2. Prerequisites¶
- A working deployment with
config.json,.env, andcompose.yaml(Deployment). 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.
Minimal template:
strategy my.grid
version 1
when book.updates:
keep.low:
place.buy(qty: $params.qty.dec, price: $price * 0.998,
post_only: true, dedup_key: "buy:initial")
See Strategy scripts for the full language (remembers, tunable,
triggers, steps, guards, and the complete 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 trading entry with your strategy name:
{
"trading": [
{
"symbol": "BTC/USDC",
"exchange": "hyperliquid",
"qty": "0.0001",
"grid_interval": [0.1, 0.5],
"strategy": "my_grid",
"testnet": true
}
]
}
The strategy 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.
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. There is currently no
per-instance override key in config.json; changing defaults means editing the strategy
file and restarting the engine.
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 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. |