Skip to content

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, and compose.yaml (Deployment).
  • docker and docker compose (or a local dio build 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

  1. Edit the .strategy file.
  2. Run dio strategy validate ... (exit 0 is good).
  3. Optionally compile and diff (dio strategy compile ...; dio strategy diff a.json b.json).
  4. Restart the engine (docker compose down && docker compose up -d).
  5. 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.