Skip to content

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, and compose.yaml (Deploying).
  • 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.

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, and order.cancel actions are the ones wired for this.
  • A primary strategy. Copy strategies/jacobs_ladder.strategy and 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

  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. 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.