Skip to content

Your first strategy

A strategy is a text file. The engine reads it at startup and turns it into orders. You do not write OCaml for this, and you do not need to understand the order router.

This walkthrough adds a small overlay to an asset that is already trading. The overlay keeps one spot buy order below the market. It is deliberately boring: it will not fill unless the price drops, so you can watch the loop work without opening a position by accident. At the end we will cancel it.

Why an overlay

A primary strategy owns the asset's grid: the buy rungs, the sell levels, the accumulated balance, all of it. An overlay is a second file attached to the same asset. It can place orders and reserve quote or base, but it does not replace the grid.

That makes an overlay the smallest useful thing to write first. If the overlay has a bug, you can delete it and the grid keeps running.

1. Create the file

Create strategies/hello_overlay.strategy:

strategy hello_overlay
version 1

tunable:
  qty: float = 0.0002
  offset_pct: float = 0.10

when book.updates:
  context:
    market.facts

  keep.bid:
    if $platform.spot.bid > 0.0:
      order.replace(market: "spot", side: "buy", qty: $params.qty,
                    price: $platform.spot.bid * (1.0 - $params.offset_pct),
                    tif: "ALO", dedup_key: "hello:buy")

Here is what each piece does. You can read the file top to bottom, which is the point of the language.

strategy hello_overlay is the name. It has to match the file name without the .strategy suffix, or the engine refuses to start.

version 1 is the language version. Leave it alone for now.

tunable: declares two numbers you can edit without touching the logic. qty is in base units, not dollars. offset_pct is a fraction, so 0.10 means 10% below the best bid.

when book.updates: says: run the steps below each time the asset gets a new top-of-book snapshot.

context: is a step label. The engine does not care what you call it. market.facts refreshes the market facts for this cycle, including $platform.spot.bid.

keep.bid: is another step. Labels are yours. Keep them short and put dots between words; the engine turns keep.bid into keep_bid internally.

if $platform.spot.bid > 0.0: is a guard. A zero bid means the book is not ready, and we do not want to price an order off a missing number.

order.replace(...) keeps one order for the key hello:buy. The first time it runs, it places the order. On later cycles it finds that same order and amends the price if the bid moved. tif: "ALO" means post-only: the order rests as a maker and will not cross the spread.

One detail that trips people up: the log will say tif=GTC even though the file says ALO. ALO is the engine's name for a post-only GTC order. The venue receives a post-only limit.

Pick a quantity above your venue's minimum notional. On BTC/USDC, 0.0002 BTC is about $17 at $85k, which clears Hyperliquid's $10 minimum. If you use a different asset or venue, adjust it.

2. Validate

From the repo root:

dio strategy validate strategies/hello_overlay.strategy

You should see:

strategy "hello_overlay" (strategies/hello_overlay.strategy): ok

If it fails, the message gives the line and the name it did not like. Fix that before you put the file anywhere near a live config. The validator catches misspelled action names, wrong argument names, and bad references. It is much cheaper than finding the typo in a log at 2am.

3. Attach it

Pick an asset group in trading.assets that does not already have an overlay. An asset group has at most one overlay. If your asset already has one, use another asset for the experiment or remove the old binding first. If every asset you trade already has an overlay, use a paper account or a spare symbol.

Add the overlay section to that group in config.json:

{
  "asset": "BTC",
  "strategy": {
    "exchange": "hyperliquid",
    "symbol": "BTC/USDC",
    "name": "jacobs_ladder",
    "qty": "0.0001"
  },
  "overlay": { "exchange": "hyperliquid", "name": "hello_overlay" }
}

overlay.exchange names the venue the overlay trades; here it is the same venue as the strategy, but it may differ when the hedge lives elsewhere. overlay.symbol is optional and defaults to the owning strategy symbol when the venues match (or <COIN>/USDC for a Hyperliquid hedge). Leave the carry params out. A custom overlay reads its tunable values from the file.

If your config still uses the legacy flat trading array, the same binding is "overlay": { "strategy": "hello_overlay" } inside the trading entry; it inherits that entry's exchange and symbol.

4. Restart and watch

Strategy files are read at startup. There is no hot reload.

docker compose down
docker compose up -d
docker compose logs -f engine

Look for the line that names your file:

strategy: BTC/USDC overlay strategies/hello_overlay.strategy

Then look for the order:

Overlay order: buy BTC/USDC 0.0002 @ 76500 tif=GTC reduce_only=false key=hello:buy

The price depends on the current bid and on the venue's tick rounding. Check the venue's open orders. You should see one spot buy, roughly 10% below the market.

If the order is not there, check three things in this order: the overlay line in the log, the minimum-notional check in the log, and the venue's open orders. The engine will skip an order below the venue minimum and keep going, so the strategy can look like it did nothing.

5. Change one thing

Edit offset_pct from 0.10 to 0.08. Validate, restart, and watch the resting order move closer to the bid.

That is the whole loop:

  1. Edit the file.
  2. Validate.
  3. Restart.
  4. Read the log and check the venue.

There is no hidden state to reset for this example. The order key is stable, so the engine amends the same order instead of stacking a new one.

6. Stop it

Do not just delete the overlay from config.json. The order is already on the venue, and removing the overlay leaves it there.

Cancel it first. Replace the step body with this:

when book.updates:
  context:
    market.facts

  cancel:
    if $platform.spot.bid > 0.0:
      order.replace(market: "spot", side: "buy", qty: 0.0,
                    price: $platform.spot.bid, tif: "ALO",
                    dedup_key: "hello:buy")

Validate, restart, and confirm the open order is gone. order.replace with qty: 0.0 cancels the resting order for that key. Then remove the overlay entry from config.json and restart once more.

You can also cancel the order in the venue's UI. The file is not the source of truth for orders that are already resting; it only manages them while it runs.

What you just used

One trigger, one fact, one action:

  • when book.updates: decides when the file runs.
  • $platform.spot.bid is the number it reads.
  • order.replace is the order it keeps on the book.

Everything else in the language is another version of that shape. You add facts to the guard, actions to the body, and remembered variables when you need to carry a decision from one cycle to the next.

Where to go next

jacobs_ladder.strategy is the full grid. Read it after this page. The step labels are the same kind of labels you just wrote, and the buy and sell phases map to the actions in the glossary.