# Position reconciliation: when a bot and exchange disagree

URL: https://fincodelabs.com/en/blog/position-reconciliation · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Backtesting and risk · Published: 2026-11-06 · Updated: 2026-11-06

Position reconciliation for trading bots: break types, what to query on Binance and Bybit, when to run it, what to do with a break, and tested Python.

**Short answer:** **Position reconciliation** means comparing the positions, open orders and fills your own system has recorded with what the exchange, broker or custodian reports, then explaining every difference. In a trading bot it runs at startup before any new order, after every reconnect and on a timer. A difference nobody can explain, called a break, should block new orders and alert a person, because the bot's picture of its own risk can no longer be trusted.

This page uses the term in its trading-software sense. Searches for order reconciliation also bring up hospitals, where it means checking a patient's orders when care changes hands, and accounting, where it means matching purchase orders to invoices.

Exchange details come from official documents read on 5 November 2026. This is a function of the account owner's own software: alerts go to a person the owner names, and nothing here recommends an exchange, a setting or a strategy. Automated trading can lose money. Rules depend on your jurisdiction and the platform's terms. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## What is position reconciliation in a trading system?

It is a repeated comparison of your records with the venue's, so a wrong belief about risk is caught before the next order relies on it.

**The same idea in three settings**

| Setting | Your record | Compared with | Usual timing |
| --- | --- | --- | --- |
| Trading bot | Local store of orders, fills and positions | Exchange snapshots and the private event stream | Startup, reconnect, timer |
| Fund or asset manager | Internal book of record | Custodian, prime broker or administrator statements | Each reporting cycle |
| Regulated algorithmic firm | Electronic trading logs | Orders and exposures reported by venues, brokers and clearing members | Real time where the other side provides it |

The last row is Article 17(3) of RTS 6, Delegated Regulation (EU) 2017/589, read on legislation.gov.uk in its UK version on 5 November 2026. It binds the investment firms it covers, not a private bot, so treat it as good practice. Order states and drop copies are in the [crypto order management system](https://fincodelabs.com/en/blog/crypto-order-management-system) guide, and limits in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

## Position reconciliation breaks: types, causes and evidence

A break is any difference between the two records that your software cannot yet explain, and most fall into seven classes.

**Break classes; field names from Binance and Bybit documents read 5 November 2026**

| Break | What you see | Usual cause | Evidence to pull |
| --- | --- | --- | --- |
| Missed fill | Position differs and the venue lists a fill you never recorded | Private stream dropped during a reconnect | Fills since your last trade ID: Binance `myTrades` with `fromId`; Bybit `execId` |
| Order on the venue, unknown locally | An open order with no local row | Crash after the send but before the write, or a blind retry | Its [client order ID](https://fincodelabs.com/en/blog/client-order-id) |
| Order in your store, gone from the venue | Your store says working; open orders disagree | Filled, canceled, expired or never arrived | A lookup by ID, since absence from open orders does not say which |
| Fee in the base asset | Spot quantity slightly short after a buy | Fee charged in the asset received | `commission` and `commissionAsset` on Binance; `execFee` and `feeCurrency` on Bybit |
| Funding payment | Cash differs, quantity does not | [Perpetual funding](https://fincodelabs.com/en/blog/perpetual-futures-funding-rate) | Binance `ACCOUNT_UPDATE` reason `FUNDING_FEE`; Bybit `execType` `Funding` |
| Liquidation or auto-deleveraging | Position cut or gone with no order from you | Margin event on the venue | Binance client IDs `autoclose-` and `adl_autoclose`; Bybit `positionStatus` `Liq` or `Adl`, `execType` `BustTrade` or `AdlTrade` |
| Manual trade | A fill under a client ID your software never issued | A person or second program on the same account | Your own ID prefix. A separate account for the bot removes this class |

Binance Spot's `myTrades` returns `orderId` but no client order ID, so join each fill to its order before classifying it. State drift also appears in [trading bot development](https://fincodelabs.com/en/blog/trading-bot-development).

## Order reconciliation: what to query on Binance and Bybit

Read open orders, fills and the position or balance, using the private stream for speed and REST snapshots as the check.

**Read-only endpoints and request weight, documents read 5 November 2026**

| Need | Binance Spot | Binance USD-M | Bybit V5 |
| --- | --- | --- | --- |
| Open orders | `GET /api/v3/openOrders`: 6 with a symbol, 80 without | `GET /fapi/v1/openOrders` and `GET /fapi/v1/openAlgoOrders`: 1 each with a symbol, 40 without | `GET /v5/order/realtime` |
| Fills | `GET /api/v3/myTrades`: 5 with `orderId`, 20 without; at most 24 hours per query | `GET /fapi/v1/userTrades`: 5; at most 7 days per query, past 3 months only | `GET /v5/execution/list`: at most 7 days per query |
| Position or balance | `GET /api/v3/account`: 20; balances, since Spot has no position | `GET /fapi/v3/positionRisk`: 5 | `GET /v5/position/list` |

Binance's note on the position call says to use it with `ACCOUNT_UPDATE` for timeliness and accuracy. Four details catch people out:

- **Stops live elsewhere on USD-M.** Binance moved conditional orders to its Algo Service on 9 December 2025, per its change log. That entry does not say whether `openOrders` still lists them, so read `openAlgoOrders` too.
- **An empty answer needs care.** Binance's V3 position call returns only symbols with a position or open orders. Bybit returns a row for a named symbol either way, but with only `settleCoin` it returns sizes above zero. Treat a failed request as unknown, never as flat. NautilusTrader's documentation, a design reference and not an endorsement, says the same: a missing report is not evidence of a flat position.
- **Lookups expire.** As read on 5 November 2026, Binance USD-M stops finding canceled or expired orders without fills after 3 days, and any order after 90 days. Bybit's history shows Cancelled and Rejected orders for 24 hours, and beyond 7 days only orders with fills; its realtime call reaches only the latest 500 closed orders, and after a server restart Unified accounts should use history.
- **Bybit's page points two ways.** Its sidebar titles the fills page "2 years", its parameters cap each query at 7 days, and its change log of 29 August 2024 ties 2 years to classic-account spot. Step back in 7-day windows and confirm the reach on your account; that needs a key and was not run.

## The position reconciliation process: when to run it

Run it at startup, after every reconnect or outage, after any order with an unknown status, and on a timer.

1. **Startup.** Read the venue's state before any strategy code can act. NautilusTrader holds back strategies while a position report stays unresolved.
2. **Reconnect.** Subscribe to the private stream first and take the snapshot second, so nothing in between is lost, and apply both by trade ID so a repeat is harmless.
3. **Unknown status.** After a timeout, settle that one order before anything else touches the symbol.
4. **Timer.** This catches unreported drift, and it has to fit the rate budget.
5. **Second read.** A difference counts only if it survives a short wait and a fresh read. Binance calls its API system asynchronous, with some delay normal, Bybit warns that its position endpoint can lag in extreme volatility, and an order in flight looks like a break.

> **Hypothetical budget.** A bot trades 5 symbols and reconciles every 30 seconds, so 2 passes a minute. On USD-M one pass costs 5 x 1 for open orders, 5 x 1 for algo orders, 5 for positions and 5 x 5 for fills: 40 weight, or 80 a minute, which is 3.3% of the 2,400 per minute in production `exchangeInfo` on 5 November 2026. On Spot one pass costs 5 x 6 + 5 x 20 + 20 = 150, or 300 a minute, which is 5% of the 6,000 in Spot `exchangeInfo` that day. This only shows the sizing.

## What to do with a position reconciliation break

Block new orders, alert a person the account owner named, keep the evidence and let that person decide; automatic closing belongs only in a rule the owner wrote.

**A starting point for the rule document; the owner sets the real policy**

| Class | The software | The person decides |
| --- | --- | --- |
| Fee in the base asset, funding | Corrects the local record and logs it | Nothing, unless the amount looks wrong |
| Missed fill under your own ID | Applies the fill and blocks until a clean pass | Whether to resume |
| Order unknown locally | Blocks and leaves the order alone | Adopt it or cancel it |
| Order gone from the venue | Looks it up by ID and blocks if there is no answer | What happens next |
| Liquidation, auto-deleveraging, manual trade | Blocks and alerts | What the position should be now |
| Unexplained difference | Blocks, alerts, saves both snapshots and the raw events | Everything |

Correcting a record and changing a position are different acts. NautilusTrader by default generates synthetic orders and fills to align its local state with the venue's report. A real order sent to "fix" a position is a trading decision made on data that has just proved unreliable, so it belongs in the owner's written rules or with the person alerted. Block only new risk: cancels and approved exits must keep working. Software maintenance covers this logic, not watching anyone's positions.

## Position reconciliation example in Python

This classifier compares local and venue dictionaries and prints one line per difference; the data is synthetic and no exchange is called.

```python
from decimal import Decimal as D

OURS = "bot1-"     # your client ID prefix
GRACE_S = 5        # illustrative allowance
BOOKABLE = {"FEE_IN_BASE", "FUNDING"}   # correct the record, no block

def classify(fill):
    cid = fill["cid"] or ""
    if cid.startswith(("autoclose-", "adl_autoclose")):
        return "FORCED_CLOSE"
    return "MISSED_FILL" if cid.startswith(OURS) else "MANUAL_TRADE"

def reconcile(local, venue, now):
    out = []
    for cid in sorted(venue["orders"].keys() - local["orders"].keys()):
        out.append(("ORDER_UNKNOWN_LOCALLY", cid))
    for cid in sorted(local["orders"].keys() - venue["orders"].keys()):
        if now - local["orders"][cid] > GRACE_S:   # younger means in flight
            out.append(("ORDER_NOT_ON_VENUE", cid))
    for sym in sorted(local["pos"].keys() | venue["pos"].keys()):
        # missing from a complete read: flat
        gap = venue["pos"].get(sym, 0) - local["pos"].get(sym, 0)
        for f in (f for f in venue["fills"] if f["sym"] == sym):
            if f["id"] in local["seen"]:    # known fill: check its fee
                if f["fee_asset"] == sym[:3]:   # real code: baseAsset from exchangeInfo
                    gap += f["fee"]
                    out.append(("FEE_IN_BASE", sym, -f["fee"]))
                continue
            gap -= f["qty"]
            out.append((classify(f), sym, f["qty"]))
        if gap:
            out.append(("UNEXPLAINED", sym, gap))
    cash = venue["cash"] - local["cash"] - venue["funding"]
    if venue["funding"]:
        out.append(("FUNDING", "USDT", venue["funding"]))
    if cash:
        out.append(("UNEXPLAINED", "USDT", cash))
    return out

def fill(i, sym, cid, qty, fee="0", asset="USDT"):
    return dict(id=i, sym=sym, cid=cid, qty=D(qty), fee=D(fee), fee_asset=asset)

local = {
    "orders": {"bot1-22": 900, "bot1-23": 998},   # client ID: time sent
    "pos": {"BTCUSDT": D("1.000"), "ETHUSDT": D("2.0"), "SOLUSDT": D("10"),
            "XRPUSDT": D("500"), "ADAUSDT": D("100")},
    "seen": {"t1"}, "cash": D("1000.00"),
}
venue = {
    "orders": {"bot1-21": "BUY 0.5 ETHUSDT"},
    "pos": {"BTCUSDT": D("0.999"), "ETHUSDT": D("2.5"), "SOLUSDT": D("13"),
            "XRPUSDT": D("0"), "ADAUSDT": D("90")},
    "fills": [fill("t1", "BTCUSDT", "bot1-20", "1.000", "0.001", "BTC"),
              fill("t2", "ETHUSDT", "bot1-19", "0.5"),
              fill("t3", "SOLUSDT", "from-the-app", "3"),
              fill("t4", "XRPUSDT", "autoclose-7", "-500")],
    "cash": D("998.75"), "funding": D("-1.25"),   # fills, funding: since last pass
}
breaks = reconcile(local, venue, now=1000)
for b in breaks:
    print(*b)
blocking = [b for b in breaks if b[0] not in BOOKABLE]
print("new orders:", "BLOCKED, alert the named person" if blocking else "allowed")
```

Output on Python 3.11, run 5 November 2026:

```
ORDER_UNKNOWN_LOCALLY bot1-21
ORDER_NOT_ON_VENUE bot1-22
UNEXPLAINED ADAUSDT -10
FEE_IN_BASE BTCUSDT -0.001
MISSED_FILL ETHUSDT 0.5
MANUAL_TRADE SOLUSDT 3
FORCED_CLOSE XRPUSDT -500
FUNDING USDT -1.25
new orders: BLOCKED, alert the named person
```

Order `bot1-23` was sent 2 seconds earlier, so it counts as in flight. The 5-second allowance and the no-block rule for fees and funding are illustrative. A real version also pages through fills and rereads before declaring a break.

## Read-only testnet calls: what ran and what did not

The read calls stop at the key check, so successful private responses were not exercised.

On 5 November 2026, signed requests to `/api/v3/openOrders`, `/api/v3/myTrades` and `/api/v3/account` on the Spot Testnet, with a placeholder key, each returned HTTP 401 and `-2015`. Signing is shown in the [Binance API Python guide](https://fincodelabs.com/en/blog/binance-api-python-trading-bot). With your own testnet key, try the cases that matter: restart with an order open, place an order by hand, and stop the stream during a fill. For code review in general, see the [trading bot code review](https://fincodelabs.com/en/services/trading-bot-code-review) page; naming Binance or Bybit here does not mean Fincode integrates them or has built on them.

## Frequently asked questions

### What does position reconciliation mean?

Comparing the positions, orders and fills in your own records with what the exchange, broker or custodian reports, and explaining every difference. A difference that stays unexplained is a break.

### What is the difference between position and order reconciliation?

Position reconciliation compares holdings, while order reconciliation compares working orders and their fills. A bot needs both, since a position break usually traces back to a missed order or fill.

### How often should a trading bot reconcile?

At startup, after every reconnect, after any unknown order status and on a timer. No exchange document read on 5 November 2026 gives an interval, so size it against your rate limit.

### Should a bot close a position automatically after a break?

Only if the account owner wrote that rule. Otherwise it should block new orders and alert a person, because a closing order sent on unreliable data is itself a trading decision.

## Sources

- [Delegated Regulation (EU) 2017/589 (RTS 6), Article 17, UK version on legislation.gov.uk](https://www.legislation.gov.uk/eur/2017/589/article/17)
- [Binance Spot API documentation: REST API (open orders, trades, account, data sources)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance USD-M Futures: Trade endpoints (order lookup limits, account trade list)](https://developers.binance.com/en/docs/catalog/core-trading-derivatives-trading-usd-s-m-futures/api/rest-api/trade)
- [Binance official Python connector, USD-M REST module (position and open order weights and notes)](https://github.com/binance/binance-connector-python/blob/master/clients/derivatives_trading_usds_futures/src/binance_sdk_derivatives_trading_usds_futures/rest_api/rest_api.py)
- [Binance USD-M Futures: User Data Streams (ACCOUNT_UPDATE, autoclose client IDs)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/user-data-streams)
- [Binance USD-M Futures: Change log (conditional orders moved to the Algo Service)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/change-log)
- [Binance USD-M Futures exchangeInfo, live rateLimits](https://fapi.binance.com/fapi/v1/exchangeInfo)
- [Bybit V5 API: Get Trade History (7-day query window)](https://bybit-exchange.github.io/docs/v5/order/execution)
- [Bybit V5 API: Get Position Info](https://bybit-exchange.github.io/docs/v5/position)
- [Bybit V5 API: Get Order History (what stays queryable and for how long)](https://bybit-exchange.github.io/docs/v5/order/order-list)
- [Bybit V5 API: Get Open and Closed Orders (latest 500 closed orders, restart note)](https://bybit-exchange.github.io/docs/v5/order/open-order)
- [Bybit V5 API: Enums (execType, positionStatus)](https://bybit-exchange.github.io/docs/v5/enum)
- [Bybit V5 API: Changelog (entry of 29 August 2024 on 2 years of trades)](https://bybit-exchange.github.io/docs/changelog/v5)
- [NautilusTrader documentation: Execution Reconciliation (design reference)](https://nautilustrader.io/docs/latest/concepts/execution/reconciliation/)
- [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/position-reconciliation

Cite as: Fincode, "Position reconciliation: when a bot and exchange disagree", updated November 6, 2026, https://fincodelabs.com/en/blog/position-reconciliation
