From clicks to code: trading the API
At some point, a systematic trader stops clicking and starts coding — because strategies need to run while you sleep, because hand-execution leaks errors and latency, and because rules you cannot automate are usually rules you have not actually defined. The shift from terminal to API feels like a graduation. It is also where a new class of expensive mistakes lives, and almost none of them are about trading. They are about software: duplicate messages, missed messages, drifting state, and code that retries its way into a position ten times larger than intended.
The engineering that prevents these failures is boring. That is a compliment — in trading systems, boring is the highest grade.
REST and websockets: pull versus push
Trading APIs, including Obsidiate’s, generally expose two transports with complementary jobs. REST is pull: your code sends a request — place this order, cancel that one, fetch balances — and gets one response. Each exchange is independent and explicit, which makes REST the right tool for commands and for snapshots of truth. Websockets are push: you open a persistent connection, subscribe to streams — order book updates, trades, the status of your own orders — and the server sends events the moment they happen, no asking required.
The division of labor follows directly: polling a REST endpoint in a loop to watch prices is slow, wasteful, and eventually rate-limited; acting on commands over a stream without explicit acknowledgment invites ambiguity. Use REST to act and to resynchronize; use websockets to watch. Most production systems use both simultaneously: orders out through REST, market data and fill confirmations flowing back over the socket.
Idempotency: the concept that saves you money
Here is the scenario that teaches every API trader this word. Your code sends an order. The network hiccups; no response arrives. Did the order reach the exchange? You do not know. If you resend and the original actually made it, you are now in the market with twice the intended position. If you do not resend and it never arrived, your strategy thinks it has a position that does not exist. Both branches are bad, and timeouts guarantee you will face the choice.
The solution is idempotency: design the operation so doing it twice has the same effect as doing it once. In practice, you attach a unique client order ID to every order you create. If you must retry, you resend with the same ID, and the exchange recognizes the duplicate — the second submission is acknowledged but not executed again. The ambiguity of an unanswered request collapses: retry with the same ID, safely, until you get a definitive answer. One small field, and an entire class of double-execution bugs becomes impossible.
Generate a fresh client order ID for every new order intent, and reuse it only for retries of that same intent. Teams that get this backwards — reusing IDs across intents, or generating new IDs on retry — rediscover why the rule exists, at market prices.
Rate limits: the API’s speed limit
Exchanges cap how many requests you may send per second, both to protect their infrastructure and to keep one runaway client from degrading the venue for everyone. Hit the cap and your requests are rejected until the window resets. The failure mode worth fearing is not the rejection itself — it is what naive code does next. A bot that responds to rejection by immediately retrying is a self-inflicted denial-of-service: every retry consumes quota, which causes more rejections, which trigger more retries. Meanwhile the request your system actually needs to send — the cancel, the exit — is stuck behind the stampede.
- Budget your requests. Know the documented limits and design normal operation to use a fraction of them, leaving headroom for the moments when you genuinely need a burst.
- Back off exponentially. On rejection, wait before retrying, and double the wait on each subsequent rejection. Add random jitter so multiple instances do not retry in lockstep.
- Prioritize. When quota is scarce, cancels and risk-reducing actions should jump the queue ahead of new orders and housekeeping queries. The request that flattens your position is never the one to defer.
Reconciliation: trust, then verify
Your trading system maintains a local picture of reality — open orders, positions, balances — and that picture will drift from the exchange’s truth. Messages arrive out of order; sockets drop and miss events during the gap; a fill lands during a reconnect. The exchange’s records are authoritative; your copy is a cache, and caches go stale. Production systems therefore reconcile: periodically, and always after any reconnect, they fetch the authoritative state via REST and compare it with local state. Discrepancies are logged loudly and resolved in the exchange’s favor. Unreconciled systems fail in the worst possible way — silently, with the strategy making confident decisions from a fictional picture of its own positions.
The boring checklist
The remaining safeguards fit in a list, and skipping any of them is a loan you will repay with interest:
- A kill switch — one command that cancels all orders and halts the system, tested regularly, reachable when everything else is on fire.
- Sanity bounds on every order — maximum size, maximum notional, price-distance-from-market checks, enforced in your code before the request leaves the building.
- Structured logging of every request, response, and stream event with timestamps — when something inexplicable happens at 3 a.m., the logs are the only witness.
- Graceful reconnection — sockets drop as a fact of life; on reconnect, resubscribe and reconcile before trading resumes.
- Start embarrassingly small — run new code with minimum size until it has survived real network weather, real rate limits, and at least one ugly market day.
Key takeaways
- REST is for commands and authoritative snapshots; websockets are for real-time watching — production systems use both.
- Idempotent order placement via client order IDs turns the deadly “did it arrive?” ambiguity into a safe retry.
- Rate limits punish naive retry loops; back off exponentially and let risk-reducing requests jump the queue.
- Your local state is a cache of the exchange’s truth — reconcile after every reconnect and on a schedule, or drift silently.
- Kill switch, order sanity bounds, full logging, graceful reconnects, tiny initial size: the checklist is dull and non-negotiable.
- In trading systems, boring engineering is not the opposite of sophistication — it is what sophistication looks like.