Skip to main content

Overview

Most trading bot tutorials show you how to place an order in a loop. That is the easy 10% of the problem, and it produces bots that work in testing and misbehave the first time something interrupts them. The hard part is state. Your bot restarts, the process was killed mid-execution, a request timed out and you do not know if it landed, or a TWAP you started an hour ago is still running somewhere. A bot that assumes it starts flat will happily double its exposure every time it comes back up. This guide builds a bot around that problem. The signal is left to you, because that is your edge and nobody can give it to you. Everything around it, the part that determines whether the bot is safe to leave running, is what we cover.
Read time: about 16 minutes. Python and REST API experience assumed. Read Place an order first if you have not placed one yet.

TL;DR

  • Reconcile before you trade. Read positions, open orders, and running strategies. Skipping the third is the classic bug.
  • Trade the delta, not the target. Compare what you want against what you already have plus what is already working.
  • One idempotency key per intended order, reused across retries, so a timeout cannot become a duplicate.
  • Managed strategies (TWAP, ICEBERG, PEGGED, TRAILING_STOP, pre-trigger STOP_LOSS) do not appear in open orders.
  • cancel-all clears CLOB orders only. Strategies keep running.
  • Parse every number with Decimal. They arrive as strings for a reason.

What you will do

  • Read complete account state across three endpoints
  • Compute a position delta that accounts for orders already working
  • Place orders with retry-safe idempotency keys
  • Choose an order type based on size rather than habit
  • Run a dry cycle that prints intended trades without placing them
  • Add a main loop that respects your rate limit and backs off on 429

What you will need

Knowledge
  • Python 3.9 or later, and comfort with REST APIs
  • A trading signal of your own. This guide deliberately does not provide one.
Tools and access
  • A Bravado API key with trade.read, trade.execute, and trade.cancel
  • pip install requests
  • USDC collateral, though the dry-run mode needs none

Architecture

Four steps, in this order, every cycle:
1

Reconcile

Read what you hold and what is already working. Never assume.
2

Evaluate

Decide the target position. Your signal lives here.
3

Diff

Target minus current equals the trade. Trade only the difference.
4

Execute

Place with an idempotency key, sized by an order type that suits the size.
The reason reconcile comes first, every cycle rather than only at startup, is that your bot is not the only thing that changes your account. A TWAP fills between cycles. A stop triggers. Somebody trades manually through the portal. Reading state fresh each pass makes all of those harmless.

Set up the client

DRY_RUN defaults to True on purpose. The expensive mistake should require a deliberate act, not an oversight. Every example below is safe to run as written.

Reconcile: read complete state

Three calls. The third is the one people miss.
A TWAP started an hour ago is still buying. It is not in orders/open, because managed strategies live on /v2/trade/strategies. A bot that reconciles against open orders alone treats that TWAP as if it does not exist and orders the same size again, which is how you end up with double the position you intended.
PENDING counts as committed. A strategy waiting on its entry conditions has not filled, but it will, and ordering more in the meantime means you get both.

Diff: trade the difference

That MIN_SHARES guard matters more than it looks. Without it, a bot whose target is 2 shares away from actual will place a 2-share order, get rejected by the venue, and retry on the next cycle, forever, burning rate limit the entire time.

Choose an order type by size

Most bots hard-code MARKET and pay for it on thin books. Choose deliberately:
The critical property of TWAP for a bot specifically: it runs on Bravado’s side. If your process dies mid-execution, the strategy keeps going. A self-hosted scheduler dies with you, halfway through a position.

Execute

Generate the key outside any retry loop. Inside, every attempt carries a different key and each one that reaches the server creates its own order. This is the single most expensive mistake in this guide. See Safe retries.

Run a dry cycle

Expected output with DRY_RUN = True:
Note the size: the target was 250 shares but the plan is 183.87, because reconciliation found 66.13 already held or working. That subtraction is the whole point of the diff step.

The main loop

Set the interval from your actual quota rather than guessing at one:

Going live

Flip DRY_RUN to False and start small. Specifically:
1

One symbol first

Run against a single market so any surprise is contained and legible.
2

Watch a restart deliberately

Kill the process mid-cycle and start it again. Confirm the next plan accounts for what is already working rather than re-ordering it.
3

Then widen

Only once a restart is boring should you point it at more markets.

Wrapping up

The bot is four steps, and three of them are bookkeeping. Reconcile, diff, execute, repeat. The signal, the interesting part, plugs into one function. That ratio is deliberate. Most bots that lose money do it through operational failures rather than a bad signal: duplicated positions after a restart, orders retried into existence, a TWAP that nobody was counting. Getting the bookkeeping right is what lets the signal be the thing that matters.

Frequently asked questions

Your bot is not the only thing changing the account. Strategies fill between cycles, stops trigger, and someone may trade manually. Reading fresh state each pass makes all of that harmless instead of requiring you to anticipate it.
A PENDING strategy has been accepted and is waiting on its entry conditions. It has not filled yet, but it will. Treating it as absent means ordering the same exposure twice and getting both.
POST /v2/trade/orders/cancel-all clears CLOB orders only. Managed strategies must be cancelled individually with DELETE /v2/trade/strategies/{id}. Iterate over /v2/trade/strategies and cancel each ACTIVE or PENDING record.
Resting orders need at least 5 shares and market orders need $1 notional. Without a minimum-size guard, a bot will retry a sub-minimum order every cycle and consume its rate limit doing so.
No. Values arrive as strings to preserve precision, and float arithmetic accumulates error that eventually produces a size the venue rejects. Decimal costs nothing here.
Check the response body rather than the status code alone. A 200 can carry warnings[] describing a partial fill or a clamped price, and bracket legs report their own failures in brackets.*.error.

Resources