Skip to content

Flatten everything

POST /api/orders/flatten is the panic button. It cancels every open order at every broker, waits until the brokers confirm the cancels, and then closes every open position with a market order at the broker that holds it. It answers that the account is flat only after the brokers' own positions show zero. It exists for the moment when you want the whole account flat now and have no time to work out what is open where.

This unwinds the whole account

Without dry_run, this route cancels every open order at all ten brokers and sends a real MARKET order for every open position. Market orders fill at whatever price is available, and nothing is retried or undone. Always look at a dry run first, and send the real request only when you mean it.

The table below lists the one route on this page.

Method Endpoint Description
POST /api/orders/flatten Cancels every open order, waits, closes every position, then waits for the positions to show zero

Why the cancels go first

The order of the two halves is the whole point of this route. Suppose you hold a long position with a stop-loss order resting below it. If the position were closed first, the stop would still be live at the exchange. When the price later fell to the stop, it would sell again and leave you short, which is a new trade that nobody chose. So the route first asks the order engine to halt every parent it is still running, so that no armed trigger, bracket or schedule places a new order while the account is being unwound, then cancels every open order, then re-reads the brokers' order books until they agree the orders are gone, and only then sends closing orders. After the closes it waits a second time, re-reading the brokers' positions until each closed one shows zero, and only then answers that the account is flat.

Flatten: cancel first, then close, then confirm The API sends a cancel for every open order to the broker holding it, then re-reads the brokers' order books in Redis every quarter second until the cancels are confirmed or the wait runs out. Only then does it send one market order per position to the broker that holds it, and it re-reads the brokers' positions every quarter second until every closed position shows zero or the wait runs out. It answers flat only when they all do. Your program confirm: FLATTEN 5. answer: flat only if positions show zero API worker POST /api/orders/flatten KillSwitch decides Redis <broker>:orders:orders <broker>:portfolio:positions 2. wait for the cancels: re-read the order books every 0.25 s 4. wait for the closes: re-read the positions until each shows zero both waits: ORDER_FLATTEN_WAIT_SECONDS Broker A open stop, long position Broker B open target Broker C short position 1. cancels go out first 3. then one MARKET close per position, same broker
Orange dots are the request, the cancels, which leave first, and the answer; blue dots are the re-reads of Redis while the route waits, first of the order books for the cancels and then of the positions for the closes; green dots are the closing market orders, which leave only after the first wait.

Flatten

POST /api/orders/flattenaccess-token

This route decides what to cancel and what to close from Redis alone. Open orders come from each broker's <broker>:orders:orders hash, and positions come from each broker's own <broker>:portfolio:positions hash rather than from the merged portfolio document, because a closing order has to go to the broker that actually holds the position.

Each cancel takes room in the per-broker rate budget before it is sent, waiting up to UNIFIED_BROKER_INTERFACE_API_ORDER_RATE_WAIT_SECONDS when the broker has already been sent its per-second limit of messages: ten by default, five for Zerodha and INDmoney, or the broker's row in unified.broker_order_costs where that sets one. A cancel that finds no room in time is reported as not sent, with the budget's message, and the route goes on to the rest.

Request parameters

The body is a JSON object with a confirmation word, so that a stray or mistyped request cannot unwind an account.

Name In Type Required Description
access-token header string yes The day's token.
confirm body string yes Must be exactly FLATTEN, in capitals. Anything else is refused with 400.
dry_run body boolean no Reports what would be cancelled and closed, and sends nothing. Unlike the other order routes, this one reads the value with Python's bool(), so any non-empty value, including the string "false", counts as a dry run. Send JSON true or leave the field out.
curl -X POST http://127.0.0.1:8080/api/orders/flatten \
  -H "access-token: $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm": "FLATTEN", "dry_run": true}'
import os

import requests

response = requests.post(
    'http://127.0.0.1:8080/api/orders/flatten',
    headers={'access-token': os.environ['ACCESS_TOKEN']},
    json={'confirm': 'FLATTEN', 'dry_run': True},
    timeout=60,
)
print(response.status_code, response.json())

Give the client a generous timeout. The route sends its cancels one after another, and it waits twice: up to UNIFIED_BROKER_INTERFACE_API_ORDER_FLATTEN_WAIT_SECONDS (5 seconds by default) for the cancels to be confirmed, and up to the same again for the closed positions to show zero. Between those waits it also waits for the order engine's answers to the closes, for up to UNIFIED_BROKER_INTERFACE_API_ORDER_PLACE_LIST_WAIT_SECONDS (25 seconds by default).

Response

The bodies below are real answers recorded by the offline suite python -m test_runs.order_flatten, which runs the route against an in-memory Redis and stubbed brokers, so no real order was involved. The suite records only the names of the timing_ms keys, so the numbers here are illustrative.

{
  "dry_run": true,
  "would_cancel": [
    {"broker": "flattrade", "order_id": "26091500000021", "status": "OPEN"}
  ],
  "would_close": [
    {
      "broker": "flattrade",
      "position_key": "RELIANCE-MIS",
      "instrument_token": "1",
      "tradingsymbol": "RELIANCE-flattrade",
      "exchange": "NSE",
      "segment": null,
      "product": "intraday",
      "quantity": 10,
      "transaction_type": "SELL",
      "close_quantity": 10
    }
  ],
  "timing_ms": {"preparation": 0.9}
}
{
  "cancelled": [
    {
      "broker": "flattrade",
      "order_id": "26091500000021",
      "sent": true,
      "outcome": "accepted",
      "status_message": null
    }
  ],
  "halted": {"halted_parents": 0},
  "still_open_after_waiting": [],
  "closed": [
    {
      "broker": "flattrade",
      "position_key": "RELIANCE-MIS",
      "instrument_token": "1",
      "tradingsymbol": "RELIANCE-flattrade",
      "exchange": "NSE",
      "segment": null,
      "product": "intraday",
      "quantity": 10,
      "transaction_type": "SELL",
      "close_quantity": 10,
      "sent": true,
      "outcome": "accepted",
      "order_id": "26091500000099",
      "status_message": null,
      "http_status": 200
    }
  ],
  "positions_still_open_after_waiting": [],
  "flat": true,
  "timing_ms": {"preparation": 262.4}
}
{
  "cancelled": [
    {
      "broker": "flattrade",
      "order_id": "26091500000021",
      "sent": true,
      "outcome": "accepted",
      "status_message": null
    }
  ],
  "halted": {"halted_parents": 0},
  "still_open_after_waiting": ["flattrade:26091500000021"],
  "closed": [
    {
      "broker": "flattrade",
      "position_key": "RELIANCE-MIS",
      "close_quantity": 10,
      "transaction_type": "SELL",
      "sent": true,
      "outcome": "accepted",
      "order_id": "26091500000099",
      "http_status": 200
    }
  ],
  "positions_still_open_after_waiting": [],
  "flat": false,
  "timing_ms": {"preparation": 5004.1}
}
{
  "cancelled": [],
  "halted": {"halted_parents": 0},
  "still_open_after_waiting": [],
  "closed": [
    {
      "broker": "flattrade",
      "position_key": "RELIANCE-MIS",
      "close_quantity": 10,
      "transaction_type": "SELL",
      "sent": true,
      "outcome": "accepted",
      "order_id": "26091500000099",
      "http_status": 200
    }
  ],
  "positions_still_open_after_waiting": ["flattrade:RELIANCE-MIS"],
  "flat": false,
  "timing_ms": {"preparation": 5003.2}
}
{
  "cancelled": [],
  "halted": {"halted_parents": 0},
  "still_open_after_waiting": [],
  "closed": [
    {
      "broker": "flattrade",
      "position_key": "RELIANCE-MIS",
      "instrument_token": "999999",
      "tradingsymbol": "RELIANCE-flattrade",
      "exchange": "NSE",
      "segment": null,
      "product": "intraday",
      "quantity": 10,
      "transaction_type": "SELL",
      "close_quantity": 10,
      "sent": false,
      "outcome": null,
      "status_message": "the broker's token does not name exactly one mapped instrument, so this position was not closed"
    }
  ],
  "positions_still_open_after_waiting": [],
  "flat": false,
  "timing_ms": {"preparation": 1.3}
}

The "cancel never confirmed" and "position still held" examples are shortened: their closed entries also carry the position fields shown in the "Flat" example. In the "position still held" example the broker accepted the close, but the position still showed 10 when the wait ended. That can mean the exchange rejected the order after the broker accepted it, or only that the broker's positions had not been refreshed yet, so look at the broker before sending anything else.

Response attributes

A dry run answers with would_cancel and would_close. A real run answers with halted, cancelled, still_open_after_waiting, closed, positions_still_open_after_waiting and flat.

Attribute Type Description
dry_run boolean true, on a dry run only.
would_cancel[] array Each order that would be cancelled, with broker, order_id and status.
would_close[] array Each position that would be closed, in the same shape as a closed entry before sending.
halted.halted_parents number How many of the order engine's open parents were halted before anything was cancelled.
halted.error string Why no parent could be halted, such as the engine not running; flatten carries on regardless.
cancelled[] array One entry per cancel attempted, with broker, order_id, sent, outcome and status_message.
cancelled[].sent boolean Whether the cancel request left the machine. false means it could not be built or sent, and status_message says why.
still_open_after_waiting[] array of strings The orders a broker still reported as live when the wait ended, written as broker:order_id.
closed[] array One entry per position, with the position's fields plus what happened.
closed[].position_key string The field of the position in <broker>:portfolio:positions.
closed[].quantity number The signed net quantity held: positive for long, negative for short.
closed[].transaction_type string SELL to close a long position, BUY to close a short one.
closed[].close_quantity number The size of the closing order, which is the absolute quantity.
closed[].product string or null The position's product, on the shared vocabulary (such as intraday).
closed[].sent boolean Whether the closing order was sent.
closed[].outcome string or null accepted, rejected or unknown, from the broker's answer.
closed[].order_id string or null The closing order's broker id, when sent.
closed[].http_status number The status POST /api/orders/place would have answered with for this close, when sent.
closed[].status_message string or null Why the close failed or was not sent.
positions_still_open_after_waiting[] array of strings The positions whose close was accepted but which a broker still reported with a quantity other than zero when the wait ended, written as broker:position_key.
flat boolean true only when every cancel was sent, no order was still open after the wait, every close was sent and accepted, and every closed position showed zero before the second wait ended.
timing_ms.preparation number Milliseconds from the request's arrival to the answer, including every broker call and the wait.

Status codes

The route checks the header and the confirmation before it reads Redis, so a request without confirm is refused even before its token is compared.

Status When
200 Everything asked for was done, or this was a dry run. flat is true. Nothing open and nothing held is also a 200.
207 Some part failed: a cancel could not be sent, an order was still live after the wait, a close was not sent or not accepted, or a closed position was still held after the wait. flat is false. Read every entry.
400 flattening cancels every order and closes every position, so it needs confirm set to FLATTEN.
401 Access token is required, Invalid access token or Access token has expired.
503 Redis could not be read: <error>, when the first read of everything fails. Nothing was sent.

A failure after the first read never changes the status to anything but 207; it is reported in the entry instead. These are the messages an entry can carry.

Entry status_message What it means
cancel <broker> has no login in Redis or <broker> has no <settings> in its Redis settings The broker cannot take a cancel right now, so none was sent.
cancel the cancel could not be built: <error> Redis did not hold a value the broker's cancel needs.
cancel the cancel could not be sent: <error> The request raised before an answer came back.
close the broker's token does not name exactly one mapped instrument, so this position was not closed unified:broker_tokens has no entry, or more than one instrument, for the position's token.
close any refusal message from POST /api/orders/place For example a lot-size problem, or <broker> cannot take this order: <reason>.
close the order engine is not running, so the order was not placed; start unified-orders@order_engine.service No close could be handed to the engine, so none was sent.
close the order engine did not answer within <n> seconds, so this may still happen; read its answer later by its intent_id The engine had not answered this close when the wait ran out; it may still be sent.

What it does, step by step

The sequence below follows a run with one open order and one position.

sequenceDiagram
    autonumber
    participant C as Your program
    participant A as API worker
    participant R as Redis
    participant E as Order engine
    participant B as Broker
    C->>A: POST /api/orders/flatten {"confirm": "FLATTEN"}
    A->>A: header present? confirm is FLATTEN?
    A->>R: one pipeline: token, logins, settings,<br/>HGETALL every order book and position book
    R-->>A: replies
    A->>A: check token<br/>KillSwitch: orders to cancel, positions to close
    A->>R: EXISTS engine lock, XADD the halt command
    E->>R: halt every open parent, RPUSH the count
    R-->>A: halted_parents, through BLPOP
    loop every open order
        A->>R: take room in the rate budget
        A->>B: cancel
        B-->>A: answer (reported, never retried)
    end
    loop every 0.25 s until confirmed or the wait runs out
        A->>R: HGETALL every order book
        R-->>A: statuses
    end
    A->>R: HGET unified:broker_tokens for each position
    A->>R: XADD every closing order in one pipeline
    par each broker's lane at the same time
        E->>B: MARKET order, opposite side, same broker
        B-->>E: answer
    end
    E->>R: RPUSH each answer onto the request's reply list
    R-->>A: the answers, through BLPOP
    loop every 0.25 s until every closed position shows zero or the wait runs out
        A->>R: HGETALL every position book
        R-->>A: quantities
    end
    A-->>C: 200 flat, or 207 with what failed

What is cancelled

Every order whose status is not COMPLETE, CANCELLED, REJECTED or EXPIRED is cancelled. An order with a status nobody has mapped is cancelled too, because an unknown status is far more likely to be a live order with a new spelling than a finished one. Cancelling a finished order costs a refusal, while leaving a live one costs a position.

The wait treats an order as gone once its status is finished, or once its entry has disappeared from the broker's hash. It stops early when every cancelled order is gone.

What is closed

Only positions reported on the NET basis are closed. A broker that reports both a day row and a net row reports the same holding twice, and closing both would double the trade. A position with no day_or_net field counts as NET, and a position whose quantity is zero is left alone.

Each closing order is an ordinary placement with these fields.

Field Value
instrument_id The one instrument unified:broker_tokens lists for <broker>:<instrument_token>
transaction_type SELL for a long position, BUY for a short one
product CNC when the position's product is delivery, MIS when it is intraday, and NRML otherwise
order_type MARKET
quantity The absolute net quantity
broker The broker that holds the position, never the selector's choice

The close goes through the same checks as POST /api/orders/place, with the broker named: the broker must still be able to take the order, and the quantity must fit the lot size. The broker exclusion list does not stop a close, because a position can only be closed where it is held.

How a close reaches the broker

The cancels are sent straight from the API worker. The closes are written to the order engine together, in one pipeline, the way a list of orders is placed, so the engine's lanes send the closes at different brokers at the same time. The route waits for their answers for the single form's wait plus a tenth of a second per close, capped at UNIFIED_BROKER_INTERFACE_API_ORDER_PLACE_LIST_WAIT_SECONDS. The intent's body carries two additions, shown below as the offline suite recorded them.

{
  "instrument_id": "11111111-1111-5111-8111-000000000001",
  "transaction_type": "SELL",
  "product": "MIS",
  "order_type": "MARKET",
  "quantity": 10,
  "broker": "flattrade",
  "synthetic": {"type": "simple", "closes_position": true}
}

The engine runs the close as a plain simple order, which sends it to the broker named in broker rather than the one the broker selector would choose. closes_position lets the close use the part of the broker's daily order cap kept for exits, as described in Daily order caps.

Only this route can name the broker. POST /api/orders/place removes a broker field from the caller's body before handing it to the engine, so a caller of that route never chooses where an order goes.

Under the hood
  • Redis keys read: last_login, settings, every <broker>:orders:orders and <broker>:portfolio:positions, then unified:broker_tokens and today's catalogue for each close.
  • Timing: each of the two waits lasts up to UNIFIED_BROKER_INTERFACE_API_ORDER_FLATTEN_WAIT_SECONDS (default 5), re-reading every 0.25 seconds. A Redis error during a re-read is skipped and the wait goes on. The positions pollers refresh every 0.5 to 1 second at most brokers, but Fyers's refreshes every 5 seconds, so a Fyers position can be reported as still held when it closed near the end of the wait.
  • What counts as closed: a position's entry showing a quantity of zero, or the entry having disappeared from <broker>:portfolio:positions. Only positions whose close was accepted are waited for, because every other close is already reported as a failure.
  • Nothing is retried. A panic button that retries takes longer to finish, so a failed cancel or close is reported and the route moves on.
  • Classes: KillSwitch decides what to cancel and close and never acts; OrdersBlueprint.flatten_everything in unified_broker_interface/blueprints/orders.py reads Redis and sends.
  • Offline check: python -m test_runs.order_flatten pins that every cancel is sent before any close, by recording every broker request in the order it left.