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.
| 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 guide, and limits in 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 | 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 |
| 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 | 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.
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.
| 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
openOrdersstill lists them, so readopenAlgoOrderstoo. - 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
settleCoinit 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.
- Startup. Read the venue's state before any strategy code can act. NautilusTrader holds back strategies while a position report stays unresolved.
- 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.
- Unknown status. After a timeout, settle that one order before anything else touches the symbol.
- Timer. This catches unreported drift, and it has to fit the rate budget.
- 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.
| 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.
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 personOrder 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. 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 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 (opens in a new tab)
- Binance Spot API documentation: REST API (open orders, trades, account, data sources) (opens in a new tab)
- Binance USD-M Futures: Trade endpoints (order lookup limits, account trade list) (opens in a new tab)
- Binance official Python connector, USD-M REST module (position and open order weights and notes) (opens in a new tab)
- Binance USD-M Futures: User Data Streams (ACCOUNT_UPDATE, autoclose client IDs) (opens in a new tab)
- Binance USD-M Futures: Change log (conditional orders moved to the Algo Service) (opens in a new tab)
- Binance USD-M Futures exchangeInfo, live rateLimits (opens in a new tab)
- Bybit V5 API: Get Trade History (7-day query window) (opens in a new tab)
- Bybit V5 API: Get Position Info (opens in a new tab)
- Bybit V5 API: Get Order History (what stays queryable and for how long) (opens in a new tab)
- Bybit V5 API: Get Open and Closed Orders (latest 500 closed orders, restart note) (opens in a new tab)
- Bybit V5 API: Enums (execType, positionStatus) (opens in a new tab)
- Bybit V5 API: Changelog (entry of 29 August 2024 on 2 years of trades) (opens in a new tab)
- NautilusTrader documentation: Execution Reconciliation (design reference) (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.