# Trading and history

URL: https://docs.fxapis.com/trading

> Orders, closes, pending orders, multi-account orders, and the history they leave.



Placing orders, and the durable record they leave.

If you read one section, read [The three failures](#the-three-failures). It is
the difference between a client that retries safely and one that occasionally
opens two positions where you asked for one.

## Placing a market order [#placing-a-market-order]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/orders/market" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "EURUSD",
    "side": "buy",
    "volume": "0.10",
    "stopLoss": "1.0890",
    "takeProfit": "1.1010",
    "deviationPoints": 20,
    "clientOrderId": "strategy-a-0417"
  }'
```

The account must be online (`ready`). An account in `warm_on_demand` mode comes
online by itself for the order; see [Accounts](/accounts).

### Symbol or instrument [#symbol-or-instrument]

`symbol` is the broker's own name for the market, exactly as that broker spells it (`XAUUSD.a`, `US30.cash`). To trade the same market across accounts at different brokers, send `instrument` instead — `XAUUSD`, `US30`, `BTCUSD` — and each order trades that account's own symbol for it. See [Instruments and symbols](/instruments).

### Volumes and prices are strings [#volumes-and-prices-are-strings]

`"0.10"`, not `0.1`. This is enforced — a JSON number is rejected with
`INVALID_REQUEST`, not quietly accepted.

A JSON number is an IEEE double. `0.1 + 0.2` is `0.30000000000000004`, and a
volume that has been through a double has already lost precision we cannot
recover. Accepting it would mean silently trading a size you did not ask for,
so the API refuses it while you can still fix it.

Use a decimal type on your side, and serialise it as a string.

### Stops are absolute prices [#stops-are-absolute-prices]

`stopLoss: "1.0890"` is a price, not a distance in pips. We check the
mistake the broker cannot see — stops on the wrong side for the direction you
meant — before anything is sent, and refuse it with `400 INVALID_REQUEST`:

* A buy's stop loss must sit below its take profit (a sell's, above).
* A pending order's stops are checked against its own price — or, for a
  stop-limit, its limit price.
* A market order's fill price is not known yet, so its stops are checked
  against the market — the ask for a buy, the bid for a sell — when fxapis has
  a recent price for the symbol on that account, and otherwise left to the
  broker. A recent price is a [live quote](#live-prices) already read for that
  account, with a tick under 10 seconds old, or else the price of an open
  position on the symbol. The order never waits for a price to be fetched.

Distance and step rules are the broker's, enforced by its `order_check` before
anything is sent; a stop it refuses comes back as `ORDER_REJECTED` with the
broker's reason.

### Always send an Idempotency-Key [#always-send-an-idempotency-key]

```
Idempotency-Key: 8f14e45f-ea8d-4b1e-9c2a-7b3d5e9f0a11
```

Without one, a request that times out leaves you stuck. You cannot tell whether
the order was placed; retrying risks a second position, and not retrying risks
none at all. With one, retrying is free.

| Situation                                     | What you get                                                               |
| --------------------------------------------- | -------------------------------------------------------------------------- |
| Same key, same body, first call finished      | The **same response**, replayed, with `idempotent-replay: true`            |
| Same key, same body, first call still running | `409 IDEMPOTENCY_IN_FLIGHT` — retry shortly                                |
| Same key, **different** body                  | `409 IDEMPOTENCY_KEY_REUSED`                                               |
| Same key, after a `SEND_FAILED`               | A genuine new attempt — nothing reached the broker, so the key is released |
| Same key, after a rejection                   | The **rejection is replayed**. To try again, use a new key                 |

That last row is the subtle one, and it is deliberate. A key is only released
when we know the broker never saw the order. If the outcome is unresolved, the
key stays claimed and a retry replays the unresolved answer rather than placing
a second order.

Keys are honoured for 24 hours and scoped to your workspace; after that a key
is released and can be used again. Use a fresh one per
order — a UUID is ideal.

## The three failures [#the-three-failures]

An order can fail in three ways, and the response tells you which.

### `SEND_FAILED` (503) — it never reached the broker [#send_failed-503--it-never-reached-the-broker]

```json
{"error":{"code":"SEND_FAILED","message":"… the order was never sent. It is safe to retry.",
 "details":[{"orderId":"...","state":"failed","retryable":true}]}}
```

We know for certain that nothing was sent to the broker. The order is
`failed`.

**Retry it.** That is the only order state where resending is safe.

### `ORDER_REJECTED` (422) — the broker said no [#order_rejected-422--the-broker-said-no]

```json
{"error":{"code":"ORDER_REJECTED","message":"The broker rejected the order: No money",
 "details":[{"orderId":"...","state":"rejected","retryable":false}]}}
```

The broker refused it, with a reason, and nothing executed. The order carries
the broker's own `retcode` — fetch it with `GET /v1/orders/{id}` and you will
see `10019` here, unmodified.

`retryable` tells you whether the reason was transient:

* **`retryable: true`** — a requote (`10004`), a moved price (`10020`), no
  quotes right now (`10021`). The market moved; sending a new order — with a new `Idempotency-Key` — is correct.
  Look at the new price first.
* **`retryable: false`** — insufficient margin (`10019`), market closed
  (`10018`), trading disabled (`10017`), an invalid volume or stop. Retrying
  collects the same rejection.

### `ORDER_UNRESOLVED` (503) — we do not know [#order_unresolved-503--we-do-not-know]

```json
{"error":{"code":"ORDER_UNRESOLVED","message":"… we do not know whether the order reached the broker. Do not resend it.",
 "details":[{"orderId":"...","state":"unknown","retryable":false}]}}
```

**Do not resend this order.** It may be live at the broker right now.

This happens when a send times out, the connection drops mid-request, or the
broker returns a code that does not say what happened — `10012 TIMEOUT` cancels
*our wait*, not necessarily the order.

Poll `GET /v1/orders/{id}`. For an order that opens a position we confirm the
result with the broker automatically and move it out of `unknown` to what
actually happened; `needsReconciliation` is `true` until then. A close, stop
change or cancel that ends `unknown` is not settled automatically — its
`stateDetail` tells you to check the position or order at the broker. See
[Confirming results](#confirming-results) below.

We are deliberately cautious here. An outcome we cannot confirm is `unknown`,
never "probably failed" — including for a return code we have never seen
before. Being wrong in this direction costs a short wait; being wrong in the
other direction costs a position you never asked for.

## Confirming results [#confirming-results]

An opening order in `unknown` is checked against the broker's deal history
automatically, every minute. A close, stop change or cancel produces no
opening deal to match, so it stays `unknown` with a `stateDetail` asking you
to check at the broker — reading an absence as "it failed" could be wrong. `POST /v1/accounts/{id}/reconcile` does it immediately for one
account.

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/reconcile" -H "Authorization: Bearer $FXAPIS_KEY"
```

```json
{"data":{"examined":1,"resolved":1,"stillUnknown":0,"ambiguous":0,
         "dealsIngested":1,"positionsOpen":1,"positionsClosed":0,"skipped":null}}
```

Safe to call at any time, and safe to call twice.

### What it resolves to [#what-it-resolves-to]

| At the broker                                 | Order becomes                           |
| --------------------------------------------- | --------------------------------------- |
| Trades covering the full volume               | `filled`                                |
| Trades covering part of it                    | `partially_filled`                      |
| No matching trade, and enough time has passed | `rejected`                              |
| No matching trade yet                         | stays `unknown` for now                 |
| More than one possible match                  | stays `unknown`, counted as `ambiguous` |

An `unknown` order never becomes `failed`: that state means the order provably
never left fxapis, and after the fact only the broker's record can say what
happened.

**When it cannot be sure, it does not guess.** If two trades at the broker
could both be your order, the order stays `unknown` and is counted in
`ambiguous` — attaching a fill to the wrong order would produce a history that
looks right and is not. Those orders need a person:

```bash
curl -sS "$FXAPIS_API/v1/orders?state=unknown" -H "Authorization: Bearer $FXAPIS_KEY"
```

The order's `stateDetail` says how many candidates there were.

### Your order comments [#your-order-comments]

fxapis adds a short tag to the start of each order's MT5 comment, which is how
the broker's record is matched to your order. The comment holds 29 characters;
your own `comment` gets the 17 left after the tag, in plain ASCII. A longer
comment is cut to fit, never refused.

### It keeps your history complete [#it-keeps-your-history-complete]

Each check also records the broker's deals for the period **whether or not
anything needed confirming** — including deals no order of yours caused — and
refreshes open positions. A position the broker no longer reports is marked
closed, not deleted.

## Closing a position [#closing-a-position]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions/2104151615/close" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"volume": "0.05"}'
```

Omit `volume` to close the whole thing.

The ticket to close is the position's `brokerPositionId`. For a position your
own order opened, that order carries it too, as `order.brokerPositionId`,
set from the fill itself when the order opens. A pending order that fills
later still gets it linked from the broker's positions and deals, usually
within a few minutes — and moves from `working` to `filled` at the same time
(see [Order states](#order-states)). While it is still `null`, find the position in
`GET /v1/accounts/{id}/positions`: a new position carries the ticket of the
order that opened it, which is that order's `brokerOrderId`.

**An idempotency key matters more here than on an opening order.** A retried
close does not close twice — on a hedging account it opens an *opposing*
position, and the account ends up with exposure nobody chose.

A close is an order. It gets its own record with `intent: "close"`, the same
three failure modes, and the same rule: `ORDER_UNRESOLVED` means **do not
resend**. An unresolved close is the case where you might believe you are flat
and not be.

Two things are refused before anything is sent:

* Closing more than the position holds. The message names both numbers.
* A partial close that would leave less than the symbol's minimum volume
  behind (`volumeMin` in its [symbol list](/instruments)) — the broker refuses
  that *after* the close, leaving a position too small to trade and too small
  to close. Close it in full, or leave at least the minimum.

## Moving a stop [#moving-a-stop]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions/2104151615/modify" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"stopLoss": "1.0890", "takeProfit": null}'
```

**`null` removes a level. Omitting the field leaves it unchanged.** An omitted
field silently clearing a stop is how protection disappears without anybody
choosing it.

Recorded with `intent: "modify"`, because widening a stop changes what the
account risks and belongs beside the trade it protects — a stop that moved
before a loss is exactly what an audit asks about.

Stops are checked against the **current market price**, not the entry: a
buy's stop loss must sit below the market and its take profit above it (a
sell's, the reverse). So moving a stop to break-even, or trailing it into
profit, is allowed. A stop the market has already passed is refused before
anything is sent, because it would close the position at once. Stops on a new
order are checked against its entry instead — see
[Stops are absolute prices](#stops-are-absolute-prices).

## Pending orders [#pending-orders]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/orders/pending" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"symbol":"EURUSD","side":"buy","kind":"limit","volume":"0.10","price":"1.0900"}'
```

| Kind         | Buy waits                          | Sell waits   |
| ------------ | ---------------------------------- | ------------ |
| `limit`      | **below** the market               | **above** it |
| `stop`       | **above** the market               | **below** it |
| `stop_limit` | stop triggers, then places a limit | same         |

A limit waits for a better price; a stop waits for a worse one and is how a
breakout is traded. When fxapis has a recent price for the symbol on that
account (see [Stops are absolute prices](#stops-are-absolute-prices)), the wrong side is refused before
anything is sent, with a message naming the order you probably meant. Without
one, the broker answers with its own `INVALID_PRICE`, which says nothing about
which way round it should have been.

Pending orders are part of paid plans; on a plan without them the request is
refused with `402 FEATURE_NOT_IN_PLAN`.

To move a working pending order — its price, stops, stop-limit price or
expiry — use `POST /v1/orders/{id}/modify`. It keeps the order's ticket, so it
keeps its place in the broker's queue; an omitted field is left alone, and
`"stopLoss": null` clears a stop. Only a `working` order can be moved.

**A placed pending order is `working`, not `filled`.** That distinction is real
money: `filled` means a position exists. Some brokers answer a pending
*placement* with `DONE`, the same code a fill returns, and reading that as a
fill would tell you you are in the market when you are waiting to be.

Cancel one with `POST /v1/orders/{id}/cancel`. If it already triggered the
broker refuses with `10035` — at that point there is a position, not an order,
and what you want is a close. A successful cancel moves the pending order to
`cancelled` and records the instruction itself as `completed`.

## Margin and profit, before you trade [#margin-and-profit-before-you-trade]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/calculate" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"kind":"margin","symbol":"EURUSD","side":"buy","volume":"0.10","price":"1.1370"}'
# → { "data": { "kind": "margin", "value": "22.74000000" } }
```

Opens nothing and is safe to call freely. The account must be online — an
offline one is refused with `409 NO_RUNTIME`. This is how you find out an order
will be refused for margin *before* sending it, rather than collecting a
`10019`. `"kind": "profit"` needs a `closePrice`.

A null `value` means the broker would not answer, which is not the same as
zero.

## Live prices [#live-prices]

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/quotes?symbols=EURUSD&instruments=XAUUSD" \
  -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": [
#     { "symbol": "EURUSD", "instrument": null, "status": "ok", "bid": "1.08450000", "ask": "1.08462000",
#       "spread": 12, "digits": 5, "brokerTime": "2026-10-15 15:00:00.123", "time": "2026-10-15T12:00:00.123Z",
#       "observedAt": "2026-10-15T12:00:00.300Z", "ageMs": 177, "stale": false, "marketOpen": true, "tradeMode": "full" },
#     { "symbol": "XAUUSD.a", "instrument": "XAUUSD", "status": "ok", "bid": "2650.10000000", … } ] }
```

The prices come from the account's own broker. Each broker quotes differently,
and these are the prices this account trades at.

* **What you can ask for:** up to 20 broker `symbols` and `instruments`
  together, comma-separated, all read in one call. An instrument is quoted as
  this account's own symbol for it. The account must be online — `ready`,
  `executing` or `degraded` (`409 ACCOUNT_NOT_READY` otherwise).
* **Every quote with a tick carries `time` (UTC), `brokerTime` (the broker's
  own clock, as its terminal shows it) and `ageMs`**, how old the tick was
  when the terminal was read at `observedAt`. Apply your own freshness rule
  from those.
* **A closed market** answers its last price with `marketOpen: false` and
  `stale: false`, never an error.
* **`stale`** is true only while the market is open and the last tick is over
  60 seconds old.
* **`marketOpen`** comes from the broker's own trading sessions. It is `null`
  (and `stale` is `false`) until the terminal has reported them, a minute or
  so after an account first comes online.
* **`tradeMode`** is what the broker allows on the symbol right now: `full`,
  `longOnly`, `shortOnly`, `closeOnly` or `disabled`. `closeOnly` and
  `disabled` allow no new positions.
* **A symbol this broker does not have** answers `status: "unknown_symbol"` in
  its place; one it has sent no price for yet, `status: "no_price"`.
* **Polling:** there is no stream. Answers are shared for a quarter of a
  second, so a quote may have been read up to 250 ms before you asked — that
  is how old the *read* is, not the price, which is what `ageMs` and `time`
  tell you. Each account's terminal may be asked 300 times a minute with a
  burst of 50, across every caller and every key; shared answers are free.
  Past that: `429 RATE_LIMITED` with `retry-after`.
* **A terminal still running an older worker** — rare — answers
  `501 CAPABILITY_UNAVAILABLE` instead. Do not retry it; an open position's
  `currentPrice` is the price meanwhile.

An order's stops are checked against this price when one was read recently:
the ask for a buy, the bid for a sell
([Stops are absolute prices](#stops-are-absolute-prices)).

## Order states [#order-states]

| State              | Meaning                                                             | Resend?                  |
| ------------------ | ------------------------------------------------------------------- | ------------------------ |
| `accepted`         | Taken, not yet sent                                                 | —                        |
| `sending`          | In flight                                                           | No                       |
| `working`          | A pending order waiting at the broker. **No position exists.**      | No — it is already there |
| `completed`        | An instruction that is not a trade finished: a cancel, a stop moved | —                        |
| `filled`           | The broker confirmed the full volume                                | No                       |
| `partially_filled` | Part filled; the rest may still fill                                | No                       |
| `rejected`         | The broker refused it. Nothing executed.                            | Only if `retryable`      |
| `failed`           | **Proven** never to have reached the broker                         | **Yes**                  |
| `unknown`          | Outcome unestablished                                               | **Never**                |
| `cancelled`        | Withdrawn before execution                                          | —                        |
| `expired`          | A pending order lapsed at the broker                                | —                        |

`failed` and `unknown` are never interchangeable. `failed` is a claim we make
only with proof.

**A pending order that triggers** moves from `working` to `filled` (or
`partially_filled`) once its fill shows up in the broker's deal history or as
a position carrying its ticket — usually within a few minutes, with one
`order.filled` webhook. `filledPrice` and `filledVolume` come from its fill
deal; when only the position is known, they are taken from it on a hedging
account (the volume only while it still equals the order's) and left `null`
on a netting account, whose position may hold other orders' fills, until the
deal arrives. A pending order the broker cancels or lets expire on its own is
not yet detected: it stays `working`.

## History [#history]

Three tables, because the questions are different.

### Orders — what you asked for [#orders--what-you-asked-for]

```bash
curl -sS "$FXAPIS_API/v1/orders?state=unknown&limit=50" -H "Authorization: Bearer $FXAPIS_KEY"
```

Every order, with the broker's return code preserved exactly as given. Filter
by `accountId`, `state`, `symbol`, and a `since`/`until` window.

`state=unknown` is the query worth building an alert on: it is everything whose
outcome is not yet established.

### Deals — what the broker actually did [#deals--what-the-broker-actually-did]

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/deals?since=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer $FXAPIS_KEY"
```

A deal is the broker's record of money changing hands. This is what a statement
reconciles against, and it is **not** one row per order:

* One order can produce several deals. A partial fill leaves more than one.
* **A deal can exist with no order of yours behind it.** A stop loss firing at
  the broker, a swap charge, a deposit, a correction, or somebody trading the
  same login from the MetaTrader desktop.

That second point is why this endpoint exists separately. A history containing
only your own orders would show an account whose balance moves for no visible
reason. `orderId` is `null` on exactly those rows, so you can tell them apart.

Ordered by the broker's own timestamp, not ours. `dealType` is named rather
than numeric — `balance`, `commission`, `buy` — because `2` means nothing on a
statement.

Deals for one order: `GET /v1/orders/{id}/deals`.

### Positions — what is open now [#positions--what-is-open-now]

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions" -H "Authorization: Bearer $FXAPIS_KEY"
```

A snapshot with `observedAt` saying when it was true. The broker is
authoritative; this is what we last saw, and the timestamp is there so you can
tell a current view from a stale one rather than assuming.

### Pagination [#pagination]

Cursor, not offset:

```bash
curl -sS "$FXAPIS_API/v1/orders?limit=100" -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": [...], "page": { "hasMore": true, "nextCursor": "2026-09-28T14:02:11.482Z" } }

curl -sS "$FXAPIS_API/v1/orders?limit=100&cursor=2026-09-28T14:02:11.482Z" -H "Authorization: Bearer $FXAPIS_KEY"
```

An offset shifts as new orders arrive, so page two would silently skip rows —
in a trading history, exactly the rows added while you were paginating.

## Retention [#retention]

Orders and deals are kept for the life of the account and are not pruned.
Disconnecting an account erases its credentials; it does not erase its history,
because that history is what a dispute or an audit is settled with.

## A worked client [#a-worked-client]

```python
import uuid, time, httpx

class OrderError(Exception): ...
class Unresolved(OrderError):
    """Do not resend. The order may be live."""

def place(client, account_id, **order):
    key = str(uuid.uuid4())

    for attempt in range(3):
        response = client.post(
            f"/v1/accounts/{account_id}/orders/market",
            json=order,
            # The SAME key on every attempt. That is the whole point: the
            # server decides whether this is a retry or a new order, and it is
            # the only party that can know.
            headers={"Idempotency-Key": key},
        )

        if response.status_code == 201:
            return response.json()["data"]

        error = response.json()["error"]
        code = error["code"]

        if code == "SEND_FAILED":
            # Proven not to have reached the broker.
            time.sleep(2 ** attempt)
            continue

        if code == "IDEMPOTENCY_IN_FLIGHT":
            time.sleep(1)
            continue

        if code == "ORDER_UNRESOLVED":
            # Never retried. Hand the order id to whatever follows up on
            # unresolved orders, and stop.
            raise Unresolved(error["details"][0]["orderId"])

        if code == "ORDER_REJECTED":
            if error["details"][0].get("retryable"):
                # The market moved: a new decision needs a NEW key. Resending
                # with this one would only replay the rejection.
                raise OrderError(f"retryable rejection: {error['message']}")
            raise OrderError(error["message"])

        raise OrderError(f"{code}: {error['message']}")

    raise OrderError("gave up after 3 attempts")
```

## Multi-account orders [#multi-account-orders]

One trade, placed on many accounts at the same moment. The API calls them
*execution waves*.

```bash
curl -sS -X POST "$FXAPIS_API/v1/execution-waves" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{
    "accountIds": ["…", "…", "…"],
    "symbol": "EURUSD", "side": "buy", "volume": "0.01",
    "weights": {"<account-id>": "0.05"},
    "barrierPolicy": "release-ready",
    "executeAt": "2026-10-01T13:30:00Z"
  }'
```

Every account is brought online first; then all the orders go out together.
Poll `GET /v1/execution-waves/{id}` for the result.

`weights` sizes the trade per account — for accounts with different balances.

### Each account reports its own result [#each-account-reports-its-own-result]

Brokers are independent of one another, so a hundred accounts cannot fill as
one transaction: fills come at slightly different prices, some accounts may be
rejected for margin, and occasionally one goes unanswered.

So a multi-account order **never reports a single success or failure.** It
settles, and `summary` counts what happened:

```json
{"state":"settled","dispatchSpreadMs":30,
 "summary":{"total":100,"filled":97,"rejected":2,"skipped":0,"unresolved":1}}
```

Each account's trade has its own `orderId` and is an ordinary order — queried,
confirmed and audited like any other. `unresolved` means the same as for a
single order: **do not resend**, we are confirming it with the broker.

### When some accounts are not online in time [#when-some-accounts-are-not-online-in-time]

| `barrierPolicy`  | What happens                                                                                         |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| `release-ready`  | Trade the accounts that are online; skip the rest. A partial entry beats a late one.                 |
| `all-or-nothing` | Trade none unless every account is online. For strategies that only make sense across the whole set. |
| `wait`           | Wait past `executeAt` until every account is online, up to `expiresAt`.                              |

`all-or-nothing` is about **sending**, not the outcome: a broker can still
reject one account's order after it was sent.

With `executeAt`, the orders go out at that time, not before. Without it, they
go out as soon as every account is online — accounts still logging in are
waited for, up to 60 seconds, after which the policy decides.

### Spread [#spread]

`dispatchSpreadMs` is the time between the first account's order going out and
the last. It is the number worth alerting on: a growing spread means accounts
are getting different prices. Every multi-account order reports its own; these
are from our benchmark against an instant broker, so a real broker adds its own
latency on top:

| accounts | spread |
| -------- | ------ |
| 10       | 17 ms  |
| 50       | 26 ms  |
| 100      | 30 ms  |

Measured on our side; each broker adds its own latency on top.

### Calling one off [#calling-one-off]

`POST /v1/execution-waves/{id}/cancel`, but only before any order has gone out.
Once orders are being sent they cannot be recalled.
