# Risk controls


Source: https://docs.stxapp.io/risk-controls/

Orders rest on the book whether or not you are watching them. These controls
decide what happens to yours when you are not the one deciding: when your
connection drops, when the game starts, when a time you picked passes, and when
play goes live. Most you set yourself; the in-play delay the exchange applies to
you.

None of them are on by default. An order placed with no expiration and no
`cancel_on_disconnect` rests on the book until you cancel it or the market
settles, whether or not your process is still running.

## Order expiration

`expiration` is an optional field on `POST /api/v1/orders`.

| `expiration` | The order expires |
| --- | --- |
| *omitted* | Never. It rests until you cancel it or the market resolves. This is the default. |
| `good_till_start` | When the event starts. |
| `good_till_time` | At `expiration_time`. |

`expiration_time` is **Unix microseconds**, not milliseconds or seconds, and is
required when `expiration` is `good_till_time`. Microseconds is unusual enough
that a value in milliseconds looks plausible and puts the expiry in 1970, which
expires the order immediately.

`good_till_start` is the one to reach for if you place orders pre-game and do not
want to trade in-play. It is keyed on the **event**, not the market, so every market
on one game expires together.

## Cancel orders on disconnect

If your process dies, your host sleeps or the network drops, the orders you left
on the book keep trading without you. The `cancel_on_disconnect` flag has the
exchange pull them when it stops hearing from your client, so a connection
failure does not leave orders working that you can no longer manage.

Unlike the others, you arm it over the WebSocket rather than on the order alone,
and it takes **both** halves to work.

### Arming it on the channel

Enabled when you join, not per request:

```json
["1", "1", "orders:{user_id}", "phx_join",
  { "cancel_on_disconnect": true, "ping_timeout": 5000 }]
```

| Parameter | Notes |
| --- | --- |
| `cancel_on_disconnect` | Boolean. Defaults to `false`. |
| `ping_timeout` | Milliseconds. **Must be an integer**; a non-integer fails the join with `{"ping_timeout": "Must be an integer"}`. Clamped to **5000–20000**; values outside the range are silently pulled to the nearest bound. |

Once joined, send a `ping` on the channel before each `ping_timeout` elapses. The
server replies `{"ping": "pong", "ttl": <ping_timeout>}` and resets the clock.
Ping at roughly 60% of the window so a single slow round trip does not cost you
the connection.

This timer is not the socket's own keep-alive. Phoenix closes an idle socket
after 60 seconds; `ping_timeout` is a separate, much shorter deadline that only
governs cancellation.

### Opting an order in

```json
POST /api/v1/orders
{
  "market_id": "855faa81-7115-4f4f-b492-389fbd8c1ed8",
  "order_type": "limit",
  "action": "buy",
  "price": "0.46",
  "quantity": "25",
  "cancel_on_disconnect": true
}
```

The body is flat. Wrapping the order in `user_order`, as some older examples do,
returns `400 market_id is required`, which reads like the
field is missing when the real problem is the wrapper. See
[Place an order](/api/rest/orders/place-order/).

:::caution[The two flags are not the same flag]
The one on the channel join arms the mechanism. The one on the order decides
whether that order is included. Arming the channel and then placing orders
without the field cancels nothing, and setting it on orders without ever joining
an armed channel cancels nothing either.
:::

The server echoes back the `ping_timeout` it actually applied, which is not
always the one you asked for:

```json
{"cancel_on_disconnect": true, "ping_timeout": 15000}
```

Read the value from that reply rather than assuming your request was honored.

:::caution[One heartbeat is not enough]
There are **two independent timers**, and the socket heartbeat only resets one
of them.

| | Resets it | Deadline | Consequence of missing it |
| --- | --- | --- | --- |
| Socket keep-alive | `heartbeat` on the `phoenix` topic | 60s | The connection closes |
| `cancel_on_disconnect` | `ping` on the `orders` topic | **5–20s**, whatever you asked for at join | **Your flagged orders are cancelled** |

A 30-second heartbeat keeps the socket up and still misses the `ping` deadline,
so your book is cancelled on a connection that never dropped. Send the channel
`ping` inside your configured `ping_timeout`, on a timer rather than in response
to traffic.
:::

### What happens when you stop pinging

1. The deadline passes and the server closes the `orders` channel: your client
   receives a `phx_error` event on that topic. The socket itself stays open.
2. A **20-second grace period** starts. Reconnect inside it and your orders are
   untouched, which is what makes a brief network blip safe.
3. If the grace expires, every order whose own `cancel_on_disconnect` is `true`
   is cancelled. Orders without the flag stay on the book.

An order flagged after your last `ping` is still covered: it is cancelled when
that `ping_timeout` expires. The same applies to orders placed once the channel
is already considered dead, so placing new orders does not stop a cancellation
that is already running.

## Cancelling in bulk

The manual equivalent, for when you are shutting down deliberately rather than
failing:

| | |
| --- | --- |
| `DELETE /api/v1/orders/all` | Everything resting on your account. |
| `DELETE /api/v1/orders/batched` | A named list. The body takes `orders`, an array of objects each carrying an `order_id`, not a flat array of ids. |

A cancel is a request, not a guarantee. An order can fill in the moment between
you sending the cancel and the exchange processing it, so reconcile against
`fills` rather than assuming a cancel left you flat.

## The in-play delay

This one you do not set. While an event is `in_progress`, incoming orders
on its markets are held in a queue before reaching the book. It exists so a
participant with a faster feed than the exchange cannot pick off prices that have
not caught up with play yet.

An order sitting in that queue reads as
[`delayed`](/concepts/market-status/): accepted, holding liability, not yet
working.

Whether it applies to a given order depends on:

- **The event must be `in_progress`.** There is no delay pre-game.
- **The market sets the duration.** Read `in_play_delay_sec` from
  `GET /api/v1/markets`; it is also pushed on the `market_updates` channel. The
  value is configured per sport, so it differs between a football market and a
  cricket one. Read the field rather than assuming a number.
- **The account must be subject to it.** It can be switched off per account, in
  which case orders are never delayed regardless of the market's value.

Because the duration is per market and the exemption is per account,
`in_play_delay_sec` on a market is the ceiling, not a promise. If you need to
know whether your own orders are being delayed, watch how long they sit at
`delayed` rather than computing it.
