Documentation

Build for the exchange.

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

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:

  1. The engine writes exactly one JSON object followed by \n.
  2. The bot writes exactly one JSON array followed by \n.
  3. 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:

FieldTypeMeaning
typestringMATCH_START, TICK, or MATCH_END
protocol_versioninteger2 for this contract
match_idstringOpaque identifier for this match
tnon-negative integerCurrent simulated tick
seqnon-negative integerPer-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:

FieldTypeMeaning
engine_versionstringEngine release that judges the match
leagueFAST or STANDARDCompute-budget population
instruments["A","B","I"]Index-to-symbol map
priorsobjectEvery public world-distribution parameter listed below
fee_scheduleobjectFill and processed-message cash adjustments
seat_pseudonymstringThis seat's match-only public name
signal_typeFACTOR or IDIOPrivate signal family assigned to this seat
earnings_schedulearrayPublic {instrument, tick} events; jump sizes remain unknown
position_limitintegerMaximum worst-case long or short lots per instrument
message_budgetintegerAlias of the free processed-message budget
think_budget_msintegerCompute tokens refilled per tick
think_burst_msintegerMaximum saved compute tokens
rate_limitobjectmessages_per_tick refill and burst capacity
base_priceintegerStarting 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

FieldTypeMeaning
booksarray of book objectsFull anonymous depth for all three instruments
tradesarray of trade objectsAttributed prints since the previous packet
bulletinsarray of bulletin objectsPublic news delivered this tick
signalssignal objectThis seat's latest private observation
fillsarray of fill objectsThis seat's fills since the previous packet
acksarray of ack objectsResults for this seat's processed actions
positionthree integersNet lots in [A, B, I]
cashinteger centiCash after fills, fees, and message charges so far
risk_charge_accruedinteger centiInventory-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}. side is BUY or SELL; tif is GTC or IOC; 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-assigned order_id returned 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:

FieldTypeMeaning
t_endintegerRealized final tick
settlementthree integersFinal settlement values for [A, B, I]
scoresarrayOne {seat_pseudonym, score_centi, placement, is_house} per seat
your_statsobjectThis 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:

[]