# OCO order: how it works and how to place one by API

URL: https://fincodelabs.com/en/blog/oco-order · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-11-06 · Updated: 2026-11-06

OCO order meaning with an example, Binance Spot POST /api/v3/orderList/oco, USD-M conditional orders in the Algo Service, and a tested Python emulation.

**Short answer:** An **OCO order** (one-cancels-the-other) links two orders so that when one is activated the other is canceled, typically a take-profit and a stop on the same position. On Binance Spot you place one with `POST /api/v3/orderList/oco`. Binance USD-M futures has no OCO endpoint as of 5 November 2026: since 9 December 2025 conditional orders go through `POST /fapi/v1/algoOrder`, and a futures OCO has to be emulated with two reduce-only conditional orders and a listener that cancels the sibling.

Forum answers about OCO on Binance futures often predate the December 2025 change, so check dates. Documents here were read on 5 November 2026. The emulation ran on synthetic events, and the Spot request stopped at the testnet's key check.

This page covers mechanics only: prices are hypothetical, and it suggests no stop, target or size. A stop is not a guaranteed price, and leverage on futures can lose the whole margin. Rules depend on your jurisdiction and the platform's terms. Leverage is not available on platforms on the list of Turkey's Capital Markets Board, SPK (Communiqué III-35/B.2, Article 14), and this page is not aimed at Turkish residents. Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## OCO order meaning and example

OCO means one-cancels-the-other: two linked orders where activating one cancels the other, so a position can carry an upper and a lower exit at once.

> **Hypothetical example.** You hold 1 unit, the last price is 100, and you place a sell OCO with a limit sell at 110 and a stop at 95.

**What happens next in the hypothetical example**

| Event | Result |
| --- | --- |
| Price reaches 110 | The limit sell fills and the stop is canceled |
| Price falls to 95 | The stop triggers and the limit sell is canceled; a stop-market fill can land below 95 |
| Price gaps from 100 to 90 | A stop-market sells near 90; a stop-limit with its limit at 94 may not fill at all |
| You cancel one leg | On Binance Spot the whole list is canceled |

## Binance OCO order API: POST /api/v3/orderList/oco

Both legs go in one signed request and the exchange links them, so your software has no race to handle.

The Spot page, read with a change log last updated 30 October 2026, gives weight 1 and an unfilled order count of 2, with one `quantity` for both legs. The older `POST /api/v3/order/oco` is marked deprecated.

**Legs and price rules for a Binance Spot OCO**

| Side | Price rule | Above leg | Below leg |
| --- | --- | --- | --- |
| SELL | Limit or take-profit price > last price > stop price | `LIMIT_MAKER`, `TAKE_PROFIT` or `TAKE_PROFIT_LIMIT` | `STOP_LOSS` or `STOP_LOSS_LIMIT` |
| BUY | Limit or take-profit price < last price < stop price | `STOP_LOSS` or `STOP_LOSS_LIMIT` | `LIMIT_MAKER`, `TAKE_PROFIT` or `TAKE_PROFIT_LIMIT` |

```python
import hashlib, hmac, re, time, urllib.parse
from decimal import Decimal
import requests

LEGAL = r"[0-9]{1,20}(\.[0-9]{1,20})?"     # from the testnet's -1100 error

def oco_params(side, qty, last, target, stop):
    """Prices as strings, hypothetical. Binance's rule, checked before sending."""
    for value in (qty, last, target, stop):
        if not re.fullmatch(LEGAL, value) or not Decimal(value):
            raise ValueError(f"not a positive plain decimal: {value}")
    t, p, s = Decimal(target), Decimal(last), Decimal(stop)
    good = t > p > s if side == "SELL" else t < p < s
    if not good:
        raise ValueError(f"{side}: price rule fails for {target}, {last}, {stop}")
    limit = {"Type": "LIMIT_MAKER", "Price": target}
    trigger = {"Type": "STOP_LOSS", "StopPrice": stop}
    above, below = (limit, trigger) if side == "SELL" else (trigger, limit)
    return {"symbol": "BTCUSDT", "side": side, "quantity": qty,
            **{"above" + k: v for k, v in above.items()},
            **{"below" + k: v for k, v in below.items()},
            "listClientOrderId": "exit-0001"}

print(oco_params("SELL", "0.001", "100", "110", "95"))
print(oco_params("BUY", "0.001", "100", "90", "105"))
for bad in [("10", "9", "1"), ("100", "inf", "95")]:
    try:
        oco_params("SELL", "0.001", *bad)
    except ValueError as err:
        print(err)

params = oco_params("SELL", "0.001", "100", "110", "95")
params["timestamp"] = int(time.time() * 1000)
query = urllib.parse.urlencode(params)
query += "&signature=" + hmac.new(b"placeholder", query.encode(), hashlib.sha256).hexdigest()
try:
    r = requests.post("https://testnet.binance.vision/api/v3/orderList/oco?" + query,
                      headers={"X-MBX-APIKEY": "x" * 64}, timeout=10)     # no real key
    print(r.status_code, r.text)
except requests.RequestException as err:
    print("network error:", type(err).__name__)
```

Output on 5 November 2026 (Python 3.11, `requests` 2.31):

```
{'symbol': 'BTCUSDT', 'side': 'SELL', 'quantity': '0.001', 'aboveType': 'LIMIT_MAKER', 'abovePrice': '110', 'belowType': 'STOP_LOSS', 'belowStopPrice': '95', 'listClientOrderId': 'exit-0001'}
{'symbol': 'BTCUSDT', 'side': 'BUY', 'quantity': '0.001', 'aboveType': 'STOP_LOSS', 'aboveStopPrice': '105', 'belowType': 'LIMIT_MAKER', 'belowPrice': '90', 'listClientOrderId': 'exit-0001'}
SELL: price rule fails for 9, 10, 1
not a positive plain decimal: inf
401 {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."}
```

The last line is the key check, so the live price rule was not exercised. Unsigned testnet requests with `inf` or `9e-05` drew `-1100` and the pattern in `LEGAL`. `exchangeInfo` showed `ocoAllowed` true for BTCUSDT on the testnet that day; check it per symbol.

- **Errors.** `-2010` with "The relationship of the prices for the orders is not correct." means the rule above failed, and "OCO orders are not supported for this symbol" means what it says. `-1165`, `-1166` and `-1196` to `-1199` name the leg on the wrong side.
- **Canceling.** `DELETE /api/v3/orderList` ends the list; so does canceling either leg.
- **IDs.** A `listClientOrderId` can be used again once the previous list is filled or completely expired, so read [client order ID](https://fincodelabs.com/en/blog/client-order-id) before retrying.
- **Gaps in the documents.** The REST table's supported values for `belowType` omit `LIMIT_MAKER`, yet `belowPrice`, the WebSocket example and the FIX table all use it. The page also does not say what a partial fill of the limit leg does to the stop; the change log entry of 15 August 2019 says a touched `LIMIT_MAKER` executes first and the stop is canceled after, without naming partial fills. Test both with your own key.

Binance also has OTO, OTOCO, OPO and OPOCO lists, where a filled working order releases pending orders. Order basics are in the [Binance API Python guide](https://fincodelabs.com/en/blog/binance-api-python-trading-bot).

## OCO order on the Binance futures API: what changed in December 2025

USD-M futures has no OCO order, and since 9 December 2025 its stop and take-profit orders live in a separate Algo Service. The `/papi/v1/margin/order/oco` route in the same change log belongs to Portfolio Margin's margin account.

**USD-M conditional orders, change log and trade page read 5 November 2026**

| Task | Now |
| --- | --- |
| Place | `POST /fapi/v1/algoOrder` with `algoType=CONDITIONAL`, `triggerPrice` and `clientAlgoId` |
| Old route | `POST /fapi/v1/order` and `/fapi/v1/batchOrders` reject `STOP_MARKET`, `TAKE_PROFIT_MARKET`, `STOP`, `TAKE_PROFIT` and `TRAILING_STOP_MARKET` with `-4120` |
| Query and cancel | `GET /fapi/v1/openAlgoOrders`; `DELETE /fapi/v1/algoOrder` |
| Events | `ALGO_UPDATE`, which has carried rejection reasons since `CONDITIONAL_ORDER_TRIGGER_REJECT` was deprecated on 15 December 2025: `NEW`, `CANCELED`, `TRIGGERING`, `TRIGGERED`, `FINISHED`, `REJECTED`, `EXPIRED` |
| Amend | Untriggered conditional orders cannot be modified |

Two official documents disagree. Binance's FAQ, updated 7 April 2026, describes a margin check at placement and another at trigger, and a limit of 10 conditional orders per symbol. The change log says there is no margin check before a conditional order triggers, and that `MAX_NUM_ALGO_ORDERS` left `exchangeInfo` on 29 December 2025, with a limit of 200 conditional orders across all symbols; that filter was absent from production `exchangeInfo` on 5 November 2026. Both agree that a leg can still fail at trigger. Check the limit on the demo host with your own key.

## How to emulate an OCO on futures, and what breaks

Place two conditional exits and have a listener cancel the survivor; nothing links them on the exchange, so every failure below is yours to handle.

**Failure cases for an emulated OCO**

| Case | What goes wrong | Handling |
| --- | --- | --- |
| Both trigger | The cancel arrives too late and two exits fire | A [reduce-only](https://fincodelabs.com/en/blog/reduce-only-order) flag stops the second from opening a position the other way |
| Triggered leg refused | `ALGO_UPDATE` `REJECTED`, for example after a margin check. Canceling the sibling at trigger would leave no exit | Cancel the sibling on the first fill, not the trigger |
| Partial fill | The sibling is sized for the whole position and cannot be amended | Cancel it and place a new one for the remainder |
| Restart with one leg open | Flat means an orphan; a position means less protection than you think | Read the position and `openAlgoOrders` before anything else |
| Listener down | Nothing cancels the sibling | Reduce-only limits the damage; run [position reconciliation](https://fincodelabs.com/en/blog/position-reconciliation) on reconnect |
| Hedge Mode | `reduceOnly` cannot be sent | Send `positionSide` |

A second route exists: `closePosition=true` on `STOP_MARKET` or `TAKE_PROFIT_MARKET` closes the whole current position when triggered, and it cannot be combined with `quantity` or `reduceOnly`. Limits and kill switches belong in a separate layer, as in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

## OCO order example in code: a tested state machine

This state machine takes synthetic events and prints the position and a listener's actions, with no network and no real order.

```python
from decimal import Decimal as D

class Pair:
    """Two conditional exits on one long position. Synthetic events only."""
    def __init__(self, qty, reduce_only=True):
        self.pos, self.reduce_only = D(qty), reduce_only
        self.legs, self.log = {"tp": "WORKING", "sl": "WORKING"}, []

    def on_event(self, leg, status, filled="0"):
        other = "sl" if leg == "tp" else "tp"
        self.legs[leg] = status
        if status in ("REJECTED", "EXPIRED"):
            self.log.append(f"ALERT {leg} {status}, {other} is {self.legs[other]}")
        elif status == "FILL":
            qty = D(filled)
            self.pos -= min(qty, self.pos) if self.reduce_only else qty
            if self.pos > 0:
                self.legs[leg] = "TRIGGERED"     # the rest still works
            if self.pos < 0:
                self.log.append(f"ALERT flipped to {self.pos}")
            elif self.legs[other] in ("WORKING", "TRIGGERED"):
                self.legs[other] = "WORKING" if self.pos else "CANCEL_SENT"   # no amend
                self.log.append(f"cancel {other}" +
                                (f", new {other} for {self.pos}" if self.pos else ""))

def run(name, pair, events):
    for event in events:
        pair.on_event(*event)
    print(f"{name}: position {pair.pos}, {pair.log}")

both = [("tp", "TRIGGERED"), ("sl", "TRIGGERED"), ("tp", "FILL", "1"), ("sl", "FILL", "1")]
run("normal", Pair("1"), [("tp", "TRIGGERED"), ("tp", "FILL", "1")])
run("both trigger, reduce-only", Pair("1"), both)
run("both trigger, plain orders", Pair("1", reduce_only=False), both)
run("partial fill", Pair("1"), [("tp", "TRIGGERED"), ("tp", "FILL", "0.4"), ("sl", "FILL", "0.6")])
run("leg rejected", Pair("1"), [("sl", "TRIGGERED"), ("sl", "REJECTED")])

def after_restart(position, open_legs):
    if position == 0:
        return f"flat: cancel {open_legs}" if open_legs else "flat: nothing to do"
    if position > 0 and len(open_legs) == 2:
        return "both legs open: resume"
    return f"open position, legs {open_legs}: block new orders, alert"

for case in [(0, ["sl"]), (1, ["tp", "sl"]), (1, ["tp"]), (1, [])]:
    print(case, "->", after_restart(*case))
```

Output on Python 3.11, run 5 November 2026:

```
normal: position 0, ['cancel sl']
both trigger, reduce-only: position 0, ['cancel sl']
both trigger, plain orders: position -1, ['cancel sl', 'ALERT flipped to -1']
partial fill: position 0.0, ['cancel sl, new sl for 0.6', 'cancel tp']
leg rejected: position 1, ['ALERT sl REJECTED, tp is WORKING']
(0, ['sl']) -> flat: cancel ['sl']
(1, ['tp', 'sl']) -> both legs open: resume
(1, ['tp']) -> open position, legs ['tp']: block new orders, alert
(1, []) -> open position, legs []: block new orders, alert
```

The third line is the reason for the flag: the same events with plain orders end in an unintended short. The synthetic venue assumes reduce-only stops at zero; whether a real leg is rejected or trimmed depends on the venue. Mapping these events to real `ALGO_UPDATE` and fill messages needs a key and was not run.

## OCO orders on other exchanges

Some venues offer a native pair and others attach take-profit and stop-loss fields to a position, so the guarantees differ.

**Documents read 5 November 2026 (alphabetical, not a ranking)**

| Venue | How | Note |
| --- | --- | --- |
| Alpaca | `order_class` `oco` | Both legs on one side, and only exit orders are supported |
| Bybit | `takeProfit` and `stopLoss` on the order, or `POST /v5/position/trading-stop` | Canceled if the position closes; changing one side of an existing pair can break the binding (see the [Bybit guide](https://fincodelabs.com/en/blog/bybit-api-python) for signing) |
| Coinbase Advanced Trade | `trigger_bracket_gtc`, `trigger_bracket_gtd` | A SELL with a limit and a stop trigger; after a fill at one level the other is disabled, and downside execution is not guaranteed |
| FIX | NewOrderList (35=E), ContingencyType (1385) = 1 | As in Binance's FIX API; other venues set their own rules |
| Hyperliquid | `grouping`: `normalTpsl` or `positionTpsl` | Status `siblingFilledCanceled` on the info endpoint marks the canceled leg |
| OKX | `POST /api/v5/trade/order-algo` with `ordType=oco` | `tpTriggerPx` and `slTriggerPx`; `cxlOnClosePos` ties it to the position |

For integration work in general, see [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration); naming Binance or the other venues here does not mean Fincode integrates them or has built on them.

## Frequently asked questions

### What does OCO mean in trading?

One-cancels-the-other: two linked orders where the activation of one cancels the other. It often pairs a take-profit and a stop on one position.

### Does Binance futures support OCO orders?

USD-M futures had no OCO endpoint as read on 5 November 2026. Conditional orders go through POST /fapi/v1/algoOrder, and pairing two of them is left to your own software.

### Can both legs of an OCO fill?

A native Spot list cancels the other leg on activation. In an emulated pair both can trigger before the cancel lands, which is why each leg is usually reduce-only.

### Is the stop in an OCO order guaranteed?

No. A stop-market leg can fill below its trigger, and a stop-limit leg may not fill at all, so an OCO does not cap a loss at the stop level.

## Sources

- [Binance Spot API documentation: REST API (order lists)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot API documentation: Error codes (OCO messages)](https://github.com/binance/binance-spot-api-docs/blob/master/errors.md)
- [Binance Spot API documentation: FIX API (NewOrderList, ContingencyType)](https://github.com/binance/binance-spot-api-docs/blob/master/fix-api.md)
- [Binance Spot API documentation: Change log (OCO launch entry of 15 August 2019)](https://github.com/binance/binance-spot-api-docs/blob/master/CHANGELOG.md)
- [Binance USD-M Futures: Trade endpoints (New Algo Order)](https://developers.binance.com/en/docs/catalog/core-trading-derivatives-trading-usd-s-m-futures/api/rest-api/trade)
- [Binance USD-M Futures: Change log (Algo Service migration, entries of 6 November, 10 and 29 December 2025)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/change-log)
- [Binance USD-M Futures: User Data Streams (ALGO_UPDATE)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/user-data-streams)
- [Binance: Summary of Failed Orders in Binance Futures (updated 7 April 2026)](https://www.binance.com/en/support/faq/summary-of-failed-orders-in-binance-futures-360039707291)
- [Bybit V5 API: Set Trading Stop](https://bybit-exchange.github.io/docs/v5/position/trading-stop)
- [Coinbase Advanced Trade API: Orders guide (bracket orders)](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/guides/orders)
- [OKX API v5: Place algo order](https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order)
- [Alpaca: Orders at Alpaca (OCO orders)](https://docs.alpaca.markets/us/docs/orders-at-alpaca)
- [Hyperliquid Docs: Exchange endpoint (order grouping)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint)
- [Resmî Gazete: SPK Communiqué III-35/B.2, 13 March 2025, no. 32840, Article 14 (Turkish)](https://www.resmigazete.gov.tr/eskiler/2025/03/20250313-6.htm)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/oco-order

Cite as: Fincode, "OCO order: how it works and how to place one by API", updated November 6, 2026, https://fincodelabs.com/en/blog/oco-order
