Machine Trading Protocol/guides/bots

Guide: your first strategy, step by step

Status
guide
Source
github.com/lgalabru/machine-trading/tree/main/specs

This guide builds two strategies and watches each play a market, one step at a time. The first trades nothing: it prints what it is handed, so you can see what a strategy works with. The second buys a dip and sells the recovery, in under twenty lines, and shows the two ways that ends. Both are in the repository, as bots/printer and bots/first-strategy; the guide to writing a bot has the rest of the details.

They trade SPCX, a perpetual on SpaceX shares, on Phoenix, through two short scenarios made for reading step by step:

Scenario Steps What happens
practice-101 5 SPCX falls 3% at step 2, holds at step 3, and is back where it started at step 4
practice-102 10 SPCX rises 3% at step 3 and is back at step 5; then it falls 3% at step 7, and 3% more at step 9, and doesn't come back

The strategy may hold up to 70 SPCX either way. mtx bot run plays practice-101 unless you name another.

1. 1. Set up

You need Rust with the wasm32-wasip2 target, just, and mtx (just install mtx in the repository); no Docker.

rustup target add wasm32-wasip2
mtx bot new ~/my-bot          # a strategy that watches the market and never trades
cd ~/my-bot

The strategy is in src/lib.rs. just build builds it into bot.wasm, a WebAssembly component: the one file the grader loads.

2. 2. A strategy that prints

Put this in src/lib.rs:

use mtx_kit::prelude::*;

#[derive(Default)]
struct Printer;

impl Strategy for Printer {
    /// Once, before the first step: what doesn't change in the session.
    fn start(&mut self, session: &Session) -> BotResult {
        println!(
            "{} for {} steps, from slot {} to {}",
            session.symbol, session.steps, session.start_slot, session.end_slot
        );
        println!(
            "the mark starts at {:.2}; I may hold up to {} {} either way",
            session.start_mark, session.cap, session.symbol
        );
        Ok(())
    }

    /// Every step: the market as it is now, and an empty order sheet.
    fn step(&mut self, market: &Market, _orders: &mut Orders) -> BotResult {
        println!(
            "mark {:.2}, oracle {:.2}, holding {}",
            market.mark, market.oracle, market.position.size
        );
        if market.last {
            println!("the last step: the account is valued after it");
        }
        Ok(())
    }
}

mtx_kit::export!(Printer);

A strategy has three hooks. start runs once, with the Session: what doesn't change. step runs at every step, with the Market as it is then and an empty order sheet. end, which this one leaves out, runs once the session is over. export! makes the strategy the component.

Run it, step by step:

mtx bot run --debug

mtx bot run builds the strategy, plays one practice session, and shows what it printed. With --debug it stops after every step:

━━ SPCX · 5 steps of 2 slots · slots 462882345–462882355 ━━━━━━━━━━━━━━━
 session  mark 143.11   oracle 143.05   cap 70   drawdown limit $1000.00
 printed  │ SPCX for 5 steps, from slot 462882345 to 462882355
          │ the mark starts at 143.11; I may hold up to 70 SPCX either way

━━ step 1 of 5 · slot 462882347 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 market   mark 143.11   oracle 143.05   index 143.05   bid 143.03 · ask 143.06
          position flat   resting 0/0   pnl +$0.00   drawdown $0.00
 printed  │ mark 143.11, oracle 143.05, holding 0
 sheet    empty: no orders
 cost     6.3K fuel · 0 rpc
  enter: next step · c: run to the end · q: stop showing steps

Each block is one step:

Row What it shows
market What step was handed: Phoenix's mark (which values the account), the oracle, the index, the book's best bid and ask, the position, the orders resting, and an estimate of the session's PnL and drawdown
printed What the strategy printed during the call, println! and eprintln! alike
sheet What it asked for
sent The orders the grader planned from it, and what became of each: ✓ landed, ✗ failed (with the venue's reason), – not sent (with why); the strategy gets the same in the next step's market.sends
cost What the call spent: fuel (about one unit per WebAssembly instruction) and rpc reads

Press enter for the next step, c to run to the end, q to stop showing steps. --auto 200 plays on, 200 ms a step; --from 40 starts showing at step 40; --scenario practice-102 plays the other scenario. The world waits for the strategy, so stopping changes nothing: the session plays exactly as it would graded.

3. 3. A first strategy: buy the dip

A shock is a sudden move; most come back. This strategy waits for the mark to fall 1% below where the session started, buys half the cap, and sells it all once the mark is back:

use mtx_kit::prelude::*;

/// How far below the start the mark must fall before buying, in percent.
const DIP_PCT: f64 = 1.0;

#[derive(Default)]
struct BuyTheDip {
    start_mark: f64,
}

impl Strategy for BuyTheDip {
    fn start(&mut self, session: &Session) -> BotResult {
        self.start_mark = session.start_mark;
        Ok(())
    }

    fn step(&mut self, market: &Market, orders: &mut Orders) -> BotResult {
        let change_pct = (market.mark / self.start_mark - 1.0) * 100.0;
        let holding = market.position.size;
        if holding == 0.0 && change_pct <= -DIP_PCT {
            println!("down {:.2}% from the start: buying", -change_pct);
            orders.target_position(market.cap / 2.0);
        } else if holding > 0.0 && change_pct >= 0.0 {
            println!("back to the start: selling");
            orders.target_position(0.0);
        }
        Ok(())
    }
}

mtx_kit::export!(BuyTheDip);

The strategy keeps state between steps: here, the starting mark, read once in start. It doesn't send orders. target_position(35.0) says where it wants to be, and the grader works out the orders. In practice-101 the dip recovers:

mtx bot run --debug
━━ step 2 of 5 · slot 462882349 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 market   mark 138.97   oracle 138.82   index 138.82   bid 141.00 · ask 138.83
          position flat   resting 0/0   pnl +$0.00   drawdown $0.00
 printed  │ down 2.89% from the start: buying
 sheet    target_position(+35)
 sent     ✓ market buy 10 ≤ 141.75
          ✓ market buy 10 ≤ 141.75
          ✓ market buy 10 ≤ 141.75
          ✓ market buy 5 ≤ 141.75
 cost     5.4K fuel · 0 rpc

━━ step 3 of 5 · slot 462882351 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 market   mark 138.85   oracle 138.82   index 138.82   bid 138.82 · ask 138.85
          position +35 @ 138.84   resting 0/0   pnl −$4.20   drawdown $4.20
 sheet    empty: no orders
 cost     600 fuel · 0 rpc

At step 2 the mark is 2.89% below the start, so the strategy asks for 35 SPCX. The grader sends four market orders: one order that crosses much of the book could exceed a transaction's budget, so target_position slices them, 10 at a time. Each may fill up to 2% past the mark (141.75). At step 3 the position is there, bought at 138.84, and the strategy asks for nothing: it holds, and the mark hasn't recovered. (The book's bid, 141.00, sits above its ask at step 2: the makers' bid takes a step to follow a 3% drop.) At step 4 the mark is back at the start, and it sells. The session ends up +$145.38.

4. 4. When the dip doesn't come back

Not every shock reverts. In practice-102, SPCX first rises 3% and comes back, which the strategy ignores: it only buys dips. Then it falls 3%, and 3% more, and stays there:

mtx bot run --scenario practice-102
│ down 2.95% from the start: buying

practice-102  −$152.72   bound $1202.12   reference —

The strategy bought the first 3% and then held all the way down, because it only sells at the start. That is the lesson of a strategy this simple: it is right about the scenarios it was written for, and has no plan for the others. A next version might cut the position past a loss, or wait for the fall to slow before buying.

5. 5. Grade it

mtx bot run plays one session. mtx bot practice grades the strategy the way the benchmark does, on all of the task's practice scenarios (a minute or two of market each, with a sudden shock somewhere in it):

mtx bot practice

A strategy is plain Rust, so you can also test it without a chain: step_offline runs one step on a market you make up and returns the sheet (Testing without a chain).

The benchmark grades the strategy on scenarios it has never seen, and a mandate it must not break: a position past the cap, or a drawdown past the limit, scores the whole grade 0. The guide to writing a bot covers those rules, the order sheet in full, and how to read anything else from the chain.

6. Glossary

Every term this guide uses is in the glossary.