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.
| 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.
| 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 |
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.
-2010with "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,-1166and-1196to-1199name the leg on the wrong side. - Canceling.
DELETE /api/v3/orderListends the list; so does canceling either leg. - IDs. A
listClientOrderIdcan be used again once the previous list is filled or completely expired, so read client order ID before retrying. - Gaps in the documents. The REST table's supported values for
belowTypeomitLIMIT_MAKER, yetbelowPrice, 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 touchedLIMIT_MAKERexecutes 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.
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.
| 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.
| Case | What goes wrong | Handling |
|---|---|---|
| Both trigger | The cancel arrives too late and two exits fire | A reduce-only 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 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.
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.
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, alertThe 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.
| 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 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; 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) (opens in a new tab)
- Binance Spot API documentation: Error codes (OCO messages) (opens in a new tab)
- Binance Spot API documentation: FIX API (NewOrderList, ContingencyType) (opens in a new tab)
- Binance Spot API documentation: Change log (OCO launch entry of 15 August 2019) (opens in a new tab)
- Binance USD-M Futures: Trade endpoints (New Algo Order) (opens in a new tab)
- Binance USD-M Futures: Change log (Algo Service migration, entries of 6 November, 10 and 29 December 2025) (opens in a new tab)
- Binance USD-M Futures: User Data Streams (ALGO_UPDATE) (opens in a new tab)
- Binance: Summary of Failed Orders in Binance Futures (updated 7 April 2026) (opens in a new tab)
- Bybit V5 API: Set Trading Stop (opens in a new tab)
- Coinbase Advanced Trade API: Orders guide (bracket orders) (opens in a new tab)
- OKX API v5: Place algo order (opens in a new tab)
- Alpaca: Orders at Alpaca (OCO orders) (opens in a new tab)
- Hyperliquid Docs: Exchange endpoint (order grouping) (opens in a new tab)
- Resmî Gazete: SPK Communiqué III-35/B.2, 13 March 2025, no. 32840, Article 14 (Turkish) (opens in a new tab)
- Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish) (opens in a new tab)
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.