You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
OKX algo orders: effective state produces an unverified full fill #30655
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.
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.
The 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
accFillSz was absent from every algo read; state was always present.
The order_failed row 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 effective row, the child order (ordId) read state: filled, accFillSz: 1. The inferred filled matched 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 (algoStatusNEW 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 let safe_order infer filled = amount and remaining = 0. Leaving filled undefined is already the current behaviour and is
not enough on its own, because the inference is driven by status == 'closed'. parse_order can stop the inference for this case, leaving filled and remaining
undefined. It cannot populate filled from 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 can fetch_order the child id from info.
state == 'partially_effective': leave filled and remaining undefined; it may
have child fills.
ordType == 'trigger' with state in live, canceled, order_failed: filled = '0' (optionally corroborated by ordId == '' and actualSz == '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 API: Get algo order details
(the endpoint fetch_order calls with {"trigger": true}; the field observations above
come from live demo reads, not from this page)
Thanks for the detailed report. It reproduces on current master (11e215fe). I ran the same payloads through okx.parseOrder in JS and got the same result: effective becomes closed with filled = amount, remaining = 0, and cost is inferred. live and order_failed give filled: undefined.
The root cause matches your analysis:
ts/src/okx.tsparseOrder reads filled only from accFillSz. Algo payloads don't have that field.
parseOrderStatus maps effective to closed, and then safeOrder (ts/src/base/Exchange.ts, the status === 'closed' branches) fills in filled and remaining.
There is one more symptom from the same code path. For a market child, price comes from safeString2 (order, 'px', 'ordPx'), so the unified price is the -1 placeholder. That's why the second row shows cost: -1.
A fix has to decide how to report effective without a second request. Options include keeping status: 'closed' but blocking the inference, or treating ordPx == '-1' as "no price". That choice belongs to the maintainers, so I'm not opening a patch right now. For now, the reliable way to get the executed quantity is to fetchOrder the child info['ordId'] (or each id in ordIdList) without trigger: true.
Activity
carlotestor commentedon Sep 27, 2026
Thanks for the detailed report. It reproduces on current master (
11e215fe). I ran the same payloads throughokx.parseOrderin JS and got the same result:effectivebecomesclosedwithfilled = amount,remaining = 0, andcostis inferred.liveandorder_failedgivefilled: undefined.The root cause matches your analysis:
ts/src/okx.tsparseOrderreadsfilledonly fromaccFillSz. Algo payloads don't have that field.parseOrderStatusmapseffectivetoclosed, and thensafeOrder(ts/src/base/Exchange.ts, thestatus === 'closed'branches) fills infilledandremaining.There is one more symptom from the same code path. For a market child,
pricecomes fromsafeString2 (order, 'px', 'ordPx'), so the unifiedpriceis the-1placeholder. That's why the second row showscost: -1.A fix has to decide how to report
effectivewithout a second request. Options include keepingstatus: 'closed'but blocking the inference, or treatingordPx == '-1'as "no price". That choice belongs to the maintainers, so I'm not opening a patch right now. For now, the reliable way to get the executed quantity is tofetchOrderthe childinfo['ordId'](or each id inordIdList) withouttrigger: true.fix: prevent unverified fills on OKX algo orders