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 helper | Meaning |
|---|---|
state.t | Current 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.position | Your net positions [A, B, I] |
state.cash | Your cash in centi-units; 100 centi equals one score unit |
state.signal_type | "FACTOR" or "IDIO" |
state.signals | Your latest private signal values, or None before refresh |
state.trades | Attributed trades since the previous packet |
state.bulletins | Public news delivered this tick |
state.fills and state.acks | Your fills and action acknowledgements |
state.risk_charge_accrued | Your 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_idis 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:
- estimate how much confidence to place in the current signal;
- move order prices and sizes back toward zero inventory;
- compare maker quotes with taker actions after fees;
- combine private estimates with news, cross-book prices, and attributed flow;
- test both quick and full manifests;
- 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
- Market model explains values, news, private signals, and visibility.
- Orders and matching gives exact validation, queue, fee, and position-limit rules.
- Scoring and ratings includes the integer risk formula and a worked score example.
- Protocol reference lists every engine and bot message.
- Replay reference defines the file structure and verification contract.
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.