Continual Exchange bot protocol
This is the complete language-neutral wire contract between the engine and a bot. Protocol version 2 is the current contract. If you use the Python SDK, it handles this transport for you; use this page when implementing another language or when inspecting raw messages.
The machine-readable JSON Schema defines every object below.
Connection and framing
For a spawned bot, the engine opens a loopback TCP listener, starts the bot with
CE_BOT_ADDR=HOST:PORT, and waits up to 10 seconds for it to connect. The bot
must connect to that address; the engine never accepts a non-loopback peer.
The connection is turn-based, UTF-8, newline-delimited JSON:
- The engine writes exactly one JSON object followed by
\n. - The bot writes exactly one JSON array followed by
\n. - Only then does the next engine turn begin.
Reply [] to MATCH_START and MATCH_END; those two response arrays are read
but ignored. Reply to each TICK with an array containing zero or more bot
messages. A response is one line for the whole turn, not one line per action.
Each frame may contain at most 1 MiB including its newline and must finish within
6 seconds. EOF, an incomplete or oversized frame, or a transport timeout marks
the bot as crashed and suspends it for the rest of the match. Invalid JSON, or a
JSON array containing a structurally invalid action, is treated as [] for that
turn. A structurally valid action that violates an exchange rule receives an
acknowledgement with accepted: false on a later TICK.
Versioning and common fields
Every engine object contains:
| Field | Type | Meaning |
|---|---|---|
type | string | MATCH_START, TICK, or MATCH_END |
protocol_version | integer | 2 for this contract |
match_id | string | Opaque identifier for this match |
t | non-negative integer | Current simulated tick |
seq | non-negative integer | Per-seat engine-message sequence; start is 0, ticks begin at 1 |
An engine release supports one protocol version. A bot may stop after receiving an unsupported version, which is recorded as a crash. Readers should ignore unknown object fields so a compatible engine release can add context without changing existing meanings. Required fields are not optional.
Prices, quantities, cash, fees, positions, and scores are integers. Money uses centi-units, so 100 centi equals one score unit. Floating point appears only in public distribution parameters and world-derived estimates such as signals and bulletin magnitudes; it is never used for money.
Instrument indices are 0 = A, 1 = B, and 2 = I.
MATCH_START: configuration for this seat
MATCH_START adds these fields:
| Field | Type | Meaning |
|---|---|---|
engine_version | string | Engine release that judges the match |
league | FAST or STANDARD | Compute-budget population |
instruments | ["A","B","I"] | Index-to-symbol map |
priors | object | Every public world-distribution parameter listed below |
fee_schedule | object | Fill and processed-message cash adjustments |
seat_pseudonym | string | This seat's match-only public name |
signal_type | FACTOR or IDIO | Private signal family assigned to this seat |
earnings_schedule | array | Public {instrument, tick} events; jump sizes remain unknown |
position_limit | integer | Maximum worst-case long or short lots per instrument |
message_budget | integer | Alias of the free processed-message budget |
think_budget_ms | integer | Compute tokens refilled per tick |
think_burst_ms | integer | Maximum saved compute tokens |
rate_limit | object | messages_per_tick refill and burst capacity |
base_price | integer | Starting reference price for all three instruments |
priors contains nominal_ticks, end_mult_lo, end_mult_hi, sigma_f,
sigma_e, beta_a, beta_b, lambda_f, factor_jump_sd, lambda_e,
idio_jump_sd, bulletin_mag_noise_sd, earnings_jump_sd, signal_sigma_lo,
signal_sigma_hi, and signal_refresh_rate.
fee_schedule contains integer-centi taker_fee_per_lot_centi,
maker_rebate_per_lot_centi, message_overage_fee_centi, and integer
free_message_budget.
TICK: the seat's observation
| Field | Type | Meaning |
|---|---|---|
books | array of book objects | Full anonymous depth for all three instruments |
trades | array of trade objects | Attributed prints since the previous packet |
bulletins | array of bulletin objects | Public news delivered this tick |
signals | signal object | This seat's latest private observation |
fills | array of fill objects | This seat's fills since the previous packet |
acks | array of ack objects | Results for this seat's processed actions |
position | three integers | Net lots in [A, B, I] |
cash | integer centi | Cash after fills, fees, and message charges so far |
risk_charge_accrued | integer centi | Inventory-risk charge accumulated so far |
A book is {instrument, bids, asks}. Each level is {price, qty}; bids are in
descending price order and asks are ascending. Ownership is not shown.
A trade is {instrument, price, qty, aggressor_side, aggressor_seat, resting_seat}. Seat numbers index the replay's per-match seat table; they are
not durable account identities.
A bulletin is {kind, asset, sign, magnitude_est}. kind is FACTOR or
IDIO; asset is null for factor news and 0 or 1 for stock-specific
news. sign is -1 or 1. magnitude_est is a noisy public estimate.
Signals are tagged objects. A FACTOR seat receives {"type":"FACTOR","s":...}.
An IDIO seat receives {"type":"IDIO","s_a":...,"s_b":...}.
A fill is {order_id, client_id, instrument, side, price, qty, is_aggressor, fee_centi}. Negative fee_centi is a fee; positive is a rebate.
An acknowledgement is {client_id, order_id, accepted, reason}. order_id is
an integer for an accepted new order and otherwise may be null; reason is
null when accepted.
Bot actions
The bot's response array may contain any mixture of:
NEW_ORDER:{type, client_id, instrument, side, price, qty, tif}.sideisBUYorSELL;tifisGTCorIOC; price is 1 through 100,000 and quantity is 1 through 50. There is no market-order type: use a crossing IOC limit when immediate execution is desired.CANCEL:{type, client_id, order_id}. Use the engine-assignedorder_idreturned with the accepted new-order acknowledgement.CANCEL_ALL:{type, client_id, scope}. Scope is{"kind":"ALL"}or{"kind":"INSTRUMENT","instrument":0}.
client_id is any non-negative integer chosen by the bot and echoed in the
acknowledgement. It is a correlation value, not order identity, and need not be
unique.
Structurally valid actions may be rejected with bad_instrument,
price_out_of_bounds, qty_out_of_bounds, position_limit, or
unknown_order. Processed rejects still consume message-rate capacity and count
toward message fees.
MATCH_END: settlement and result
MATCH_END adds:
| Field | Type | Meaning |
|---|---|---|
t_end | integer | Realized final tick |
settlement | three integers | Final settlement values for [A, B, I] |
scores | array | One {seat_pseudonym, score_centi, placement, is_house} per seat |
your_stats | object | This bot's full match statistics |
placement is 1 through 6 for competitor seats and null for house seats.
your_stats contains terminal_wealth_centi, risk_charge_centi,
score_centi, fees_paid_centi, rebates_centi, message_count, fills,
final_position, suspended, crashed, and overrun_ms.
Tick eligibility, compute, and same-tick order
For each simulated tick, the reference runner queries active competitor seats sequentially. It sends one seat's private packet, reads that seat's response, records the elapsed milliseconds, and then queries the next seat. No market state advances while responses are collected. A slow seat can delay when the next process receives its packet in wall-clock time, but every seat sees the same already-fixed simulated tick and its own timer begins only when its packet is sent. Wall-clock waiting is not simulated market time.
An on-budget response to tick t is enqueued for tick t + 1. If the response
exceeds the seat's saved compute tokens by d milliseconds, eligibility moves
to t + 1 + ceil(d / think_budget_ms). That delay affects only that seat's
actions. The recorded milliseconds and actions make verification possible
without measuring wall time again.
At each tick the engine seed-permutes competitor seats once. In that seat order, it refills the seat's message tokens and drains ready actions from that seat's FIFO queue until its tokens run out. Remaining actions keep their order for a later tick. Orders then follow price-time priority in the book.
Complete example exchange
Each line below is one complete wire frame. Whitespace has been removed to make the newline boundary visible.
Engine → bot (MATCH_START):
{"type":"MATCH_START","engine_version":"0.1.0","protocol_version":2,"match_id":"example-01","t":0,"seq":0,"league":"STANDARD","instruments":["A","B","I"],"priors":{"nominal_ticks":10000,"end_mult_lo":0.9,"end_mult_hi":1.1,"sigma_f":60.0,"sigma_e":30.0,"beta_a":1.0,"beta_b":0.8,"lambda_f":0.0006666666666666666,"factor_jump_sd":20.0,"lambda_e":0.0004,"idio_jump_sd":15.0,"bulletin_mag_noise_sd":10.0,"earnings_jump_sd":30.0,"signal_sigma_lo":8.0,"signal_sigma_hi":80.0,"signal_refresh_rate":0.0005},"fee_schedule":{"taker_fee_per_lot_centi":50,"maker_rebate_per_lot_centi":20,"free_message_budget":500,"message_overage_fee_centi":5},"seat_pseudonym":"P3","signal_type":"FACTOR","earnings_schedule":[{"instrument":0,"tick":2400},{"instrument":1,"tick":7100}],"position_limit":100,"message_budget":500,"think_budget_ms":50,"think_burst_ms":500,"rate_limit":{"messages_per_tick":10,"burst":30},"base_price":1000}
Bot → engine:
[]
Engine → bot (TICK):
{"type":"TICK","protocol_version":2,"match_id":"example-01","t":17,"seq":18,"books":[{"instrument":0,"bids":[{"price":999,"qty":8}],"asks":[{"price":1002,"qty":5}]},{"instrument":1,"bids":[{"price":997,"qty":3}],"asks":[{"price":1001,"qty":6}]},{"instrument":2,"bids":[],"asks":[{"price":1001,"qty":2}]}],"trades":[{"instrument":0,"price":1001,"qty":2,"aggressor_side":"BUY","aggressor_seat":1,"resting_seat":6}],"bulletins":[{"kind":"FACTOR","asset":null,"sign":1,"magnitude_est":12.4}],"signals":{"type":"FACTOR","s":9.7},"fills":[],"acks":[{"client_id":40,"order_id":321,"accepted":true,"reason":null}],"position":[0,0,0],"cash":0,"risk_charge_accrued":0}
Bot → engine (two actions in one response frame):
[{"type":"NEW_ORDER","client_id":41,"instrument":0,"side":"BUY","price":1002,"qty":2,"tif":"IOC"},{"type":"CANCEL_ALL","client_id":42,"scope":{"kind":"INSTRUMENT","instrument":1}}]
Engine → bot (MATCH_END):
{"type":"MATCH_END","protocol_version":2,"match_id":"example-01","t":10037,"seq":10039,"t_end":10037,"settlement":[1042,1028,1035],"scores":[{"seat_pseudonym":"P1","score_centi":8420,"placement":1,"is_house":false},{"seat_pseudonym":"P2","score_centi":6130,"placement":2,"is_house":false},{"seat_pseudonym":"P3","score_centi":4480,"placement":3,"is_house":false},{"seat_pseudonym":"P4","score_centi":990,"placement":4,"is_house":false},{"seat_pseudonym":"P5","score_centi":-1120,"placement":5,"is_house":false},{"seat_pseudonym":"P6","score_centi":-3600,"placement":6,"is_house":false},{"seat_pseudonym":"H1","score_centi":1250,"placement":null,"is_house":true},{"seat_pseudonym":"H2","score_centi":-430,"placement":null,"is_house":true}],"your_stats":{"terminal_wealth_centi":5020,"risk_charge_centi":540,"score_centi":4480,"fees_paid_centi":900,"rebates_centi":240,"message_count":387,"fills":26,"final_position":[4,-2,1],"suspended":false,"crashed":false,"overrun_ms":0}}
Bot → engine:
[]