Documentation

Build for the exchange.

Game rules, protocol, SDK, scoring, and local development. Everything here describes the public competition surface.

Your first Continual Exchange bot

This walkthrough starts with a bot that stays connected, then adds a small signal-based strategy, local replay verification, and submission packaging. You need Python 3.9 or newer. No trading background is assumed.

Staging status: the Python SDK is public on PyPI. The engine repository and public-season submissions are not open yet, so a new public reader can install the SDK and write a bot now, but the local-match steps require engine-source access until that repository is released. Staging accounts, ratings, and submissions are test-only.

1. The game in one minute

Six competitor bots and two disclosed house bots trade three synthetic instruments: stocks A and B, and an index I derived from their average. Each instrument has a continuous limit order book with integer prices and price-time priority.

  • Hidden values move when public factor, stock-specific, or earnings news occurs.
  • Each competitor receives one private signal family. A FACTOR seat sees a noisy estimate of the component shared by both stocks. An IDIO seat sees noisy estimates of the two stock-specific components.
  • You know your signal family, but not its noise level. Signal quality changes from match to match.
  • The match ends near tick 10,000 at a time not announced in advance. Positions settle at the final hidden values.
  • Score is terminal wealth minus a quadratic charge for carrying inventory over time. Six-way placement, not the raw score difference, updates the rating.

The book shows aggregate depth without owner names. Completed trades identify both sides with pseudonyms that last for one match. A useful strategy combines its private signal with public news, cross-instrument prices, and the tape.

FAST and STANDARD use the same market. FAST allows 5 ms of measured response time per tick; STANDARD allows 50 ms. Start with STANDARD while learning the protocol. The league guide explains burst and overrun behavior.

2. Install the SDK and engine

Pin the released SDK version used by this guide:

python -m pip install continual-exchange-sdk==0.2.2

Local matches also need the Rust engine. Once its source repository is released:

git clone https://github.com/continualresearch/continual-exchange.git
(cd continual-exchange/engine && cargo build --release)
export CE_ENGINE_ROOT="$PWD/continual-exchange"

CE_ENGINE_ROOT tells the SDK where to find the ce executable and the public parameter manifests. A parameter manifest is the versioned JSON file holding all match settings: timing, distributions, fees, limits, compute budgets, house bots, and rating defaults.

3. Write the minimum valid bot

Create one directory that will be both your local working directory and your submission source:

mkdir my-bot

Save this as my-bot/bot.py:

from ce_sdk import Bot, run_bot


class MyBot(Bot):
    def on_tick(self, state):
        return []  # zero actions is always valid


run_bot(MyBot())

The engine starts your process with CE_BOT_ADDR=HOST:PORT. run_bot connects to that loopback address and handles newline-delimited JSON framing. It calls:

  • on_start(match_start) once for match configuration;
  • on_tick(state) for each observation, expecting a list of zero or more actions;
  • on_end(match_end) once after settlement.

The bot above never trades, but it implements the complete process lifecycle.

4. Run and verify a local match after the source release

Save this runner as my-bot/run_me.py:

import os
import sys
from pathlib import Path

from ce_sdk import run_local_match, verify_replay

engine_root = Path(os.environ["CE_ENGINE_ROOT"])
result = run_local_match(
    bots=[f"{sys.executable} my-bot/bot.py"],
    seed="hello",
    league="STANDARD",
    manifest=str(engine_root / "testkits/manifests/quick.json"),
    out="mymatch.cer",
)

print(result["summary"])
verify_replay(result["replay"])
print("replay verified")

Then run:

python my-bot/run_me.py

The quick manifest shortens development matches to 1,000 ticks. Before judging a strategy, also test the full testkits/manifests/base.json manifest over many seeds. A single match is not representative because the hidden world, signal family, signal quality, and seat order all change.

The output file uses the .cer extension, short for Continual Exchange Replay. Drop it into the replay room to inspect depth, positions, the attributed tape, and hidden values revealed after settlement. verify_replay is the stronger check: it re-executes the recorded inputs and requires the result to match exactly.

5. Understand the tick state

state is a TickState view over one TICK packet:

Field or helperMeaning
state.tCurrent tick number
best_bid(i), best_ask(i), mid(i)Top-of-book helpers for instrument i; each may be None
state.books[i]Full aggregate bid and ask depth
state.positionYour net positions [A, B, I]
state.cashYour cash in centi-units; 100 centi equals one score unit
state.signal_type"FACTOR" or "IDIO"
state.signalsYour latest private signal values, or None before refresh
state.tradesAttributed trades since the previous packet
state.bulletinsPublic news delivered this tick
state.fills and state.acksYour fills and action acknowledgements
state.risk_charge_accruedYour inventory-risk charge so far, in centi-units

Instrument indices are A=0, B=1, and I=2. The exact wire fields are in the protocol reference.

6. Send orders

The SDK supplies typed constants and action builders:

from ce_sdk import BUY, SELL, GTC, IOC, new_order, cancel, cancel_all

new_order(client_id, instrument, side, price, qty, tif=GTC)
cancel(client_id, order_id)
cancel_all(client_id, instrument)  # instrument=None clears all three books
  • client_id is an integer chosen by your bot and echoed in the acknowledgement.
  • An accepted new order receives an engine order_id; store it for precise cancellation.
  • Price is an integer from 1 through 100,000. Quantity is 1 through 50 lots.
  • A GTC order trades where marketable and leaves any remainder resting.
  • An IOC order trades immediately where possible and cancels the remainder.
  • There is no market-order message. Use a crossing IOC limit when you want an immediate trade.

Taker fills cost 0.50 score units per lot. Maker fills receive a 0.20 rebate per lot. The first 500 processed messages are free; later processed messages cost 0.05 score units each. A rejected message still counts as processed.

7. Add a small signal strategy

Replace my-bot/bot.py with this example. It handles both signal families, estimates the three hidden values, and sends a small IOC only when the visible price is far enough from that estimate.

from ce_sdk import BUY, SELL, IOC, Bot, new_order, run_bot

MARGIN = 4
SIZE = 2
SOFT_LIMIT = 90  # leaves room below the hard 100-lot limit


class SignalStarter(Bot):
    def on_start(self, match_start):
        self.signal_type = match_start["signal_type"]
        self.base = match_start["base_price"]
        self.client_id = 0

    def fair_values(self, state):
        signal = state.signals or {}
        if self.signal_type == "FACTOR":
            factor = signal.get("s")
            if factor is None:
                return None
            return [
                self.base + factor,
                self.base + 0.8 * factor,
                self.base + 0.9 * factor,
            ]

        stock_a = signal.get("s_a")
        stock_b = signal.get("s_b")
        if stock_a is None or stock_b is None:
            return None
        value_a = self.base + stock_a
        value_b = self.base + stock_b
        return [value_a, value_b, (value_a + value_b) / 2]

    def on_tick(self, state):
        fair = self.fair_values(state)
        if fair is None:
            return []

        actions = []
        for instrument in range(3):
            bid = state.best_bid(instrument)
            ask = state.best_ask(instrument)
            position = state.position[instrument]

            if ask is not None and ask < fair[instrument] - MARGIN and position < SOFT_LIMIT:
                self.client_id += 1
                actions.append(new_order(self.client_id, instrument, BUY, ask, SIZE, IOC))
            elif bid is not None and bid > fair[instrument] + MARGIN and position > -SOFT_LIMIT:
                self.client_id += 1
                actions.append(new_order(self.client_id, instrument, SELL, bid, SIZE, IOC))

        return actions


run_bot(SignalStarter())

This is a teaching example, not a claim of profitability. Its fair-value estimate ignores signal noise, public news uncertainty, inventory, queue position, and the information in other participants' trades. Those are natural places to improve it.

8. Evaluate before packaging

The runner already launches my-bot/bot.py, so rerun it over many seeds after each change:

python my-bot/run_me.py

Inspect cases where the bot accumulated inventory, crossed the spread repeatedly, or trusted a poor signal. Useful next steps include:

  1. estimate how much confidence to place in the current signal;
  2. move order prices and sizes back toward zero inventory;
  3. compare maker quotes with taker actions after fees;
  4. combine private estimates with news, cross-book prices, and attributed flow;
  5. test both quick and full manifests;
  6. measure average and worst-case response time under the chosen league.

Readable reference implementations live in ce_sdk.bots: NoiseTrader, NaiveQuoter, and SignalFollower. They are examples, not hidden game logic.

9. Package now; smoke-test after the engine source release

Put your bot and its dependencies in one directory, then create a reproducible submission zip:

ce-package "python bot.py" \
  --source ./my-bot \
  --out submission.zip

The package records the declared entrypoint in submission.json, rejects unsafe archive paths and symlinks, and enforces the 4 GiB uncompressed limit. Packaging does not require the engine.

After the engine source repository opens, add the optional smoke test:

ce-package "python bot.py" \
  --source ./my-bot \
  --out submission.zip \
  --smoke

The smoke test runs the packaged entrypoint in a Bronze validation: four competitor seats and four disclosed house bots. It requires at least one valid message, no crash or suspension, and a replay that verifies exactly.

Use the same entrypoint in local tests and the uploaded package. That catches code which accidentally depends on a development working directory, untracked file, or environment variable.

10. Continue from here

When public submissions open, the dashboard will use the same build and Bronze validation steps described above. Until then, anything shown on staging is test data and does not qualify a bot for a public season.