OKX algo orders: effective state produces an unverified full fill
Operating System
Linux
Programming Language
Python
CCXT Version
4.5.84
Description
Summary
OKX algo order responses (/api/v5/trade/order-algo, orders-algo-pending,
orders-algo-history) carry no accFillSz field, and accFillSz is the only field
okx.parse_order reads filled from. For an algo order it therefore passes
filled: None to safe_order.
For state: 'effective', parse_order_status returns 'closed'. safe_order then
infers filled = amount and remaining = 0, and computes cost from that inferred
fill. effective means the trigger fired and a child order was placed. It says nothing
about how much of that child executed. With a limit child (orderPx on create, ordPx
in the response) the child may rest unfilled or fill partially, yet ccxt reports the
algo order fully filled. The real fill belongs to the child order (ordId), which
needs its own fetch_order.
A secondary consequence of the same missing field: ordType: 'trigger' orders that
provably executed nothing (live, canceled, order_failed) parse with
filled: None. A consumer cannot tell "unknown" from "zero".
Also affects myokx and okxus, which inherit okx.parse_order.
I searched existing issues for okx algo/trigger filled, accFillSz and order_failed
and found no matching report.
Reproduction (no credentials needed)
import ccxt
ex = ccxt.okx()
ex.verbose = True # parse_order makes no request, so this logs nothing
market = {"id": "SOL-USDT-SWAP", "symbol": "SOL/USDT:USDT", "base": "SOL", "quote": "USDT",
"settle": "USDT", "type": "swap", "spot": False, "swap": True, "contract": True,
"contractSize": 0.01, "precision": {}}
base = {"algoId": "1", "instId": "SOL-USDT-SWAP", "instType": "SWAP", "ordType": "trigger",
"side": "sell", "posSide": "net", "sz": "100", "triggerPx": "90",
"cTime": "1700000000000", "uTime": "1700000001000", "ordIdList": []}
cases = [
("effective", {"ordPx": "89", "actualSz": "100", "actualPx": "89", "ordId": "42"}), # limit child
("effective", {"ordPx": "-1", "actualSz": "100", "actualPx": "", "ordId": "43"}), # market child
("live", {"ordPx": "89", "actualSz": "0", "ordId": ""}),
("canceled", {"ordPx": "89", "actualSz": "0", "ordId": ""}),
("order_failed", {"ordPx": "-1", "actualSz": "0", "ordId": ""}),
]
for state, extra in cases:
o = ex.parse_order(dict(base, state=state, **extra), market)
print(state, extra["ordPx"], o["status"], o["filled"], o["remaining"], o["cost"])verbose is on as the template asks, but this reproduction calls only the parser, so it
makes no HTTP request and produces no verbose request/response log.
Output on ccxt 4.5.84 (columns: state, ordPx, status, filled, remaining, cost):
effective 89 closed 100.0 0.0 89.0
effective -1 closed 100.0 0.0 -1.0
live 89 open None None None
canceled 89 canceled None None None
order_failed -1 canceled None None NoneThe first row has a limit child (ordPx: 89), the second a market child (ordPx: -1).
Nothing in either effective payload reports an executed quantity. filled,
remaining and cost are all inferred from status == 'closed'. In the market-child
row, cost is computed from the -1 market-price placeholder.
Observed on OKX demo trading
fetch_order(id, symbol, {"trigger": True}) on reduce-only ordType: trigger stops
with a market child (orderPx: -1). The table below is a summary of fields copied from
the parsed order's info, not a raw transcript: verbose was not enabled during these
reads. A sanitized verbose transcript can be captured and attached on request.
state |
actualSz |
ordId |
failCode |
ccxt status / filled |
|---|---|---|---|---|
live |
"0" |
"" |
0 |
open / None |
canceled |
"0" |
"" |
0 |
canceled / None |
order_failed |
"0" |
"" |
51169 |
canceled / None |
effective |
"1" |
child order id | 0 |
closed / 1.0 |
accFillSzwas absent from every algo read;statewas always present.- The
order_failedrow came from a stop that fired after its position had been closed. The reduce-only child was rejected (51169), so no child order exists. - In the
effectiverow, the child order (ordId) readstate: filled,accFillSz: 1. The inferredfilledmatched in this observation because the child was fully filled.
Comparison: binanceusdm
binance.parse_order defaults a missing executed quantity to '0', so a Binance
conditional order that never fired (algoStatus NEW or CANCELED) parses with
filled: 0.0, where the equivalent OKX read gives None. This only shows a different
normalization of a missing quantity. It does not show that executed algo orders are
reliably distinguishable on Binance.
Suggested fix
In okx.parse_order, for an algo order (algoId present, accFillSz absent):
state == 'effective': do not letsafe_orderinferfilled = amountandremaining = 0. Leavingfilledundefined is already the current behaviour and is not enough on its own, because the inference is driven bystatus == 'closed'.parse_ordercan stop the inference for this case, leavingfilledandremainingundefined. It cannot populatefilledfrom the child order, because that needs a second request. Given the one-request-per-unified-method guideline in CONTRIBUTING.md, the child's fill (ordId/ordIdList) is probably best left to the caller, who canfetch_orderthe child id frominfo.state == 'partially_effective': leavefilledandremainingundefined; it may have child fills.ordType == 'trigger'withstateinlive,canceled,order_failed:filled = '0'(optionally corroborated byordId == ''andactualSz == '0'). This rule is limited to the trigger orders observed above. Other algo types (conditional,oco,move_order_stop,iceberg,twap) were not tested.
Keying on the raw state matters: parse_order_status maps order_failed →
canceled and partially_effective → open, so the unified status alone cannot make
these distinctions.
References
Source links are pinned to ccxt master at 11e215fe.
okx.parseOrderreadsfilledonly fromaccFillSz: ts/src/okx.ts#L4325 (method starts at L4107)okx.parseOrderStatusmapseffective→closed: ts/src/okx.ts#L4099Exchange.safeOrderinfersfilled = amountandremaining = '0'for a closed order: ts/src/base/Exchange.ts#L4889-L4902 (method starts at L4743)okx.fetchOrderroutes{"trigger": true}to the algo endpoint: ts/src/okx.ts#L4425-L4427- OKX API: Get algo order details
(the endpoint
fetch_ordercalls with{"trigger": true}; the field observations above come from live demo reads, not from this page)
Source: ccxt/ccxt