Part 1 of our engineering series on programmatic trading. We build and test the exchange's systems — and the client software that talks to them — every day; this series shares what that work has taught us, so that whether you are automating a simple strategy or running a full market-making desk, you start with the habits professionals rely on.
Every serious trading operation eventually outgrows the browser. Prices move faster than a human can click, quotes need to refresh around the clock, and risk checks need to run on every order — not just the ones you remember to double-check. That is what the API is for.
But there is a large gap between calling an API and operating a trading connection. The first is an afternoon of work. The second is a discipline. This article walks through that discipline: how professional traders and market makers actually connect to an exchange like Coincall, and the practices that separate a robust system from one that fails at the worst possible moment.
The two planes: REST and WebSocket
Every exchange connection has two halves, and knowing which to use is the first professional habit.
REST is for requests. You ask a question or issue an instruction, and you get exactly one answer: fetch instruments, read your positions, submit a quote, cancel an order. REST is synchronous, explicit, and easy to reason about — but it is pull-based. Polling REST for market data is slow, wasteful, and will eventually run into rate limits.
WebSocket is for streams. You open one long-lived connection, subscribe to channels — order books, mark prices, your own position and quote events — and the exchange pushes updates to you as they happen. This is where market data and account events belong.
The professional pattern is simple: WebSocket for anything continuous, REST for anything transactional. Use REST to discover which instruments exist; use WebSocket to learn what is happening to them. A system that polls REST for prices is fighting the architecture; a system that streams is working with it.
And take those rate limits seriously from day one. Every serious venue bounds how many requests you may send per unit of time — the matching engine everyone depends on has to be protected from accidental floods. Staying inside the limits is mostly a design habit, not throttling code: stream instead of polling, batch where the API allows it, cache what rarely changes, and when the exchange tells you to slow down (HTTP 429 or a venue error code), actually slow down. The current limits for each endpoint live in the API documentation; read them before you size any loop that touches REST.
Authentication: sign exactly what you send
Coincall authenticates API requests with an HMAC-SHA256 signature. The idea is standard across professional trading venues: your API secret never travels over the wire. Instead, you use it to sign each request, and the exchange recomputes the same signature to verify it came from you.
The signature covers a canonical string built from the request:
GET/open/futures/position/get/v1?symbol=BTCUSD&uuid=<apiKey>&ts=<millis>&x-req-ts-diff=5000
GET/open/futures/position/get/v1?symbol=BTCUSD&uuid=<apiKey>&ts=<millis>&x-req-ts-diff=5000
1. Parameter order is fixed. The exchange must rebuild the exact same string you signed, so ordering cannot be ambiguous. Sort your own query parameters alphabetically, then append
uuid, ts, and x-req-ts-diff in that fixed order — exactly as the string above shows. The rule is the same order every time, and the canonical string is the reference. This is universal: every HMAC-signed exchange API has a canonicalization rule, and "signature mismatch" errors are almost always canonicalization bugs.
2. The signed bytes and the sent bytes must be identical. If you sign a raw value but URL-encode it on the wire — or vice versa — the exchange computes a different digest and rejects you. In our own client we go as far as re-reading the assembled URL after construction and refusing to connect if it differs by a single byte from what was signed. Build the string once, sign it, send it. Never let a serialization library "helpfully" reformat what you already signed.
3. Timestamps bound replay. Every signed request carries a millisecond timestamp (
ts) and a drift window (x-req-ts-diff), which travels both as a header and inside the signed string — the documentation gives the current default. A request signed too far from server time is rejected. Two practical consequences: keep your clock disciplined with NTP, and if you retry a request, re-sign it with a fresh timestamp — replaying an old signature is both a security smell and a rejection waiting to happen.The signed values travel in headers — the API key in
X-CC-APIKEY, the uppercase-hex digest in sign, plus ts and x-req-ts-diff — and the same idea extends to WebSocket: the connection URL itself carries a signed query, built from a signing input of its own (the documentation has the exact format), so authentication completes at the handshake. There is no separate "login" message to sequence around.Best practice: pin your signing code with golden test vectors — known inputs with the exact expected prehash string and digest, captured once and never hand-edited. Generate them with a dummy key and secret, never your live credentials: the prehash string contains your API key. Signing code is the one place where a refactor that "looks equivalent" can silently break everything. A golden vector turns that silent break into a failing test.
Staying connected is not the same as staying alive
A WebSocket that is open is not necessarily a WebSocket that is working. Connections go quiet — a NAT timeout, a dropped route, a stalled proxy — while both ends still believe the socket is fine. Professionals therefore treat liveness as something you measure, not assume.
Two mechanisms work together:
- Heartbeats you send. Coincall expects periodic application heartbeats (
{"action":"heartbeat"}). Send them well inside the venue's idle threshold — the documentation gives the current timeout; our own systems heartbeat every 20 seconds or faster. Cutting it close means one delayed frame disconnects you. - Silence you detect. The subtler half: track when you last received anything. If the inbound side goes quiet past your threshold, send a probe and arm a timer. If nothing comes back within your response timeout, declare the connection dead and tear it down yourself. A connection that cannot prove it is alive should be treated as dead — waiting for TCP to notice can take minutes you don't have.
One trap worth calling out for asynchronous designs: if your heartbeat only fires when your event loop polls the connection, then a consumer that stalls on slow work — a blocking database write, an expensive calculation — can starve its own heartbeat and disconnect itself. Either keep the read loop's work strictly non-blocking, or isolate liveness from processing.
Failure is normal: reconnect like you mean it
Disconnects are not exceptional events. They are routine, and your architecture should treat them that way. Three practices matter:
Back off exponentially, with jitter. On reconnect failure, double your delay each attempt up to a cap (we use ceilings in the minutes, not seconds). Then randomize: pick a uniform delay between zero and the current ceiling rather than the ceiling itself. Why jitter? Because when an exchange restarts, every client disconnects at once — and without jitter, every client also reconnects at once, in synchronized waves that look like a DDoS and prolong the outage. Full jitter breaks the thundering herd.
Reset backoff on proven health, not on connection. A subtle but important refinement: don't reset your backoff counter just because a connection succeeded. A venue in trouble will happily accept your connection and drop it five seconds later — and a naive reset turns your careful backoff into a rapid-fire hammer. Reset only after the connection has demonstrated health: sustained data delivery over a meaningful window (we use 60 seconds of proven inbound traffic).
Rebuild, don't limp. If a subscription fails after reconnect — the venue rejects it, or the acknowledgement never arrives — do not continue with a partially subscribed connection. A session that is half-alive is worse than one that is dead, because it looks healthy while silently missing data. Tear it down and rebuild the full subscription set on a fresh connection. Sessions should be all-or-nothing.
Trust the stream, verify the silence
Once data flows, the question becomes: how do you know your view of the market is still correct?
On Coincall's options and futures order-book channels, every frame carries the complete book at channel depth — each update replaces your local state rather than patching it.
This is a forgiving contract: there are no incremental deltas to mis-apply, no sequence gaps to reconcile, and a missed frame costs you freshness rather than correctness. (On venues that do stream deltas with sequence numbers, gap detection and snapshot recovery become your problem — one more reason to read each channel's data contract carefully rather than assuming they all work alike.)
Forgiving is not the same as free, though. Validate every frame at the boundary before it touches your trading logic:
- Check the identity. Does this frame belong to the channel and symbol you subscribed to? Route by the wire discriminants the venue provides; never assume.
- Check the numbers. Prices and sizes should parse to finite, positive values. A
NaNthat slips into a pricing calculation will propagate silently and corrupt everything downstream. - Check for the impossible. A book where the best bid crosses the best ask is telling you something is wrong — with the feed, or with your decoding. Refuse it rather than trade on it.
- Check freshness — against the right yardstick. A quiet order book is not a dead connection: a far out-of-the-money option can legitimately sit unchanged for minutes. Judge the connection by its heartbeats; judge the data by each channel's own rhythm and timestamps. Conflating the two leads to either panic-reconnecting on healthy feeds or calmly trusting frozen ones.
And decode tolerantly but honestly: ignore fields you don't recognize (venues add fields; your integration shouldn't break when they do), but fail loudly on fields you need that are missing or malformed. "Fail on the unprovable, tolerate the additive" is the decoding rule we build to.
The hardest problem: when you don't know what happened
Here is the scenario that separates professional order management from everything else. You submit a quote. The request times out. Did it land?
You genuinely do not know. The request may have died on the way to the exchange — or it may have been accepted, with only the response lost. These two worlds require opposite actions, and guessing wrong is expensive in both directions: retry blindly and you may end up with duplicate exposure; give up blindly and you may walk away from a live quote you no longer know you own.
The professional answer has three parts:
1. Classify every failure before reacting. Not all errors are equal, and your retry logic should know the difference:
- Transient — network hiccups, 5xx, 429, gateway timeouts. For reads and other repeat-safe requests, retry with backoff. A state-changing request that times out is never merely transient — it belongs in the ambiguous bucket below.
- Persistent — application-level rejections: bad parameters, permissions, validation. Retrying the same request will fail the same way; these need a human or a code fix, and enough of them in a row should trip your kill switch.
- Ambiguous — a state-changing request that may or may not have reached the exchange. Never blind-retry these.
- Conflict — the exchange says the state you wanted already exists. Often not a failure at all: reconcile and move on.
A corollary: only retry automatically what is safe to repeat. Reads are idempotent — retry them freely, within your rate budget. A quote creation is not — one attempt, then resolve.
2. Resolve ambiguity by asking, not guessing. When a state-changing request ends ambiguously, query the exchange for the actual state before doing anything else. Keep your own record of exactly what you sent — instrument, side, size, price, time — so you can recognize your order in the exchange's answer; and if your venue lets you attach your own identifier to an order at placement, use it — matching on an ID you chose beats matching on attributes, especially with several similar orders working. And respect what a read can and cannot prove: absence from a single open-orders snapshot does not prove your create failed — the order may simply not be visible yet. Allow a grace window and confirm with a targeted read before concluding anything.
3. Reconcile continuously. Even with perfect ambiguity handling, your local picture and the exchange's truth will drift — a missed WebSocket event, a race, an operator action from another terminal. Run a periodic reconciliation loop (ours runs every minute) that fetches the exchange's view of your open orders and quotes, adopts what you didn't know about, and cancels orphans you no longer want.
The exchange's state is the truth; your local state is a cache. Systems that forget this discover it during their first incident.
Safety rails: assume your own code is the threat
Market makers run automation that can commit capital thousands of times a day. The professionals' response is layered, fail-closed safety — controls that assume the bug will happen and bound its damage:
- Dry-run by default. New deployments should compute everything — prices, risk checks, intended quotes — and submit nothing until you explicitly flip the switch. The default state of any trading system should be "not trading."
- A pre-trade gate that fails closed. Every outgoing order passes one final checkpoint: quantity caps, notional caps, price sanity (finite, positive, above a floor), market-data freshness. And critically — if the gate itself throws an exception, the answer is reject, not "proceed." Make the unsafe path structurally impossible: one pattern we like is structuring the code so that an order which hasn't passed the gate cannot even be handed to the submission function.
- A kill switch with teeth. Track consecutive persistent failures; past a threshold, stop sending new orders, cancel the open ones, and confirm those cancellations actually took effect — cancel requests can fail while the venue is unreachable, so keep retrying at a polite pace and alert a human rather than assuming success. What to do about remaining positions is a human decision, made with human eyes. And make recovery deliberate: a kill switch that auto-resets is just a rate limiter. Require a human restart.
- Cancel-all at the edges. On startup, cancel everything before quoting — you may be inheriting orders from a crashed predecessor. On shutdown, cancel everything again. And be honest about the limit: a process that has crashed outright cancels nothing, because it is no longer running — which is exactly why the startup cancel-all matters, and why it is worth checking the documentation for venue-side protections available to your account. Never leave quotes you aren't watching.
- Stale data is a rejection. If your market data is older than your threshold — or you can't determine its age at all — you don't have a price, you have a memory. Don't quote on it.
None of these rails is sophisticated. Their power is in being unconditional.
Credentials: treat your API keys like the money they control
Your API key and secret are your account. The hygiene rules are non-negotiable:
- Keys live in the environment, never in code. Load from environment variables or an untracked
.envfile; keep.envin.gitignoreand commit only an.env.examplewith the names, not the values. - Secrets get a type. Wrap keys in a secret type (
SecretStr,SecretString, or your language's equivalent) so a stray debug print shows[REDACTED]instead of your secret. Mark sensitive headers as sensitive so your HTTP stack won't echo them either. - Redact before you log. WebSocket auth travels in the URL — which means a naive "connection failed to {url}" log line leaks your signature and key. Scrub sensitive parameters from every error message and logged payload, by rule, not by memory.
- Scope and separate. Request only the permissions each system needs — a market-data reader has no business holding trade permissions. Run distinct roles on distinct keys with no fallback between them, so a bug in one can never borrow the authority of another.
-
Encrypt, always. Production traffic is
https/wssonly. The only defensible cleartext exception is loopback in local testing — and even that is worth enforcing in code rather than convention.
Test offline, verify live — and know the difference
The final professional habit is about evidence.
Ordinary tests never touch the venue. Your default test suite should run entirely offline: golden vectors for signing, recorded real frames for decoders, local mock servers that assert the exact bytes your client sends. This keeps tests fast, deterministic, and safe to run on every change. One hard-won lesson on mocks: a hand-written fake exchange is an oracle that shares your code's beliefs — if you misread the API docs, your fake embodies the same misreading and your tests pass anyway. Recorded real responses falsify your assumptions; invented ones flatter them. Just sanitize recorded frames before they become fixtures: private streams carry your account identifiers and positions.
Live verification is deliberate and separate. When you do need to prove behavior against the real venue, make it an explicit, opt-in run — a separate executable, never something a routine test command can trigger by accident — read-only wherever possible, and pointed away from production for anything that mutates state (check the documentation for what test facilities are available to your account). And record outcomes honestly in three states, not two: pass (the evidence supports the claim), fail (the evidence contradicts it), and inconclusive (the run never established the conditions to judge — a setup error, a permission problem, a dropped connection). The third state is the one amateurs skip. A test that couldn't run is not a test that passed, and an empty result set proves nothing.
The mindset behind the mechanics
Strip away the specifics and the practices in this article reduce to a handful of principles:
- Sign exactly what you send. Canonicalization is the whole game.
- Measure liveness; never assume it. Silence is a failure mode.
- Back off with jitter; reset only on proven health. Be a good citizen of the venue.
- Validate at the boundary. Nothing unverified touches trading logic.
- The exchange is the truth; resolve ambiguity by asking it. Your state is a cache.
- Fail closed, everywhere. The default answer to uncertainty is "don't trade."
- Keys are money. Handle them like it.
- Offline tests for logic, deliberate opt-in runs for live truth. And admit "inconclusive."
Nothing here requires exotic infrastructure. What it requires is taking failure seriously before it happens — which is, more than anything else, what distinguishes professional connectivity from a script that worked in the demo.
In the next article in this series, we'll go deeper on WebSocket market data: subscription management at scale, staleness detection, and how to structure a market-data service that hundreds of strategies can trust.
Coincall provides API access for options and futures trading. Full API documentation is available at docs.coincall.com. Technical details in this article — endpoints, headers, signature formats, and limits — are illustrative and current as of publication; the official documentation is authoritative and parameters may change. Nothing in this article is investment advice. This series reflects engineering practices from our own infrastructure work; adapt them to your own risk tolerance and always test thoroughly before trading live.
Comments
0 comments
Please sign in to leave a comment.