WebSockets
The exchange pushes real-time updates over a WebSocket endpoint at /ws on the same host as the REST API:
wss://api.testnet.mnx.fi/ws (testnet)
wss://api.app.mnx.fi/ws (production)The protocol is topic-based publish/subscribe: you send a SUBSCRIBE message listing topics, and the server pushes a JSON message whenever an event occurs on one of them.
Subscribing
const ws = new WebSocket('wss://api.testnet.mnx.fi/ws')
ws.onopen = () => {
ws.send(
JSON.stringify({
type: 'SUBSCRIBE',
topics: ['prices:1', 'market:1:orderbook'],
})
)
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
// Server events: { topics: string[], timestamp, data: { type, ... } }
// Control acks: { type: 'SUBSCRIBED' | 'SUBSCRIBE_REJECTED' | ... }
}The server acknowledges accepted topics with { "type": "SUBSCRIBED", "topics": [...], "subscription_read_after": { "shard_id": "...", "shard_seq": "..." } }. The subscription_read_after coordinate is your catch-up floor: events after it arrive on the socket, and state before it must come from a REST snapshot (see below). The acknowledgement is not itself a snapshot.
Topics that are invalid, name an unknown market, or are not yours come back in a { "type": "SUBSCRIBE_REJECTED", "topics": [...], "message": "...", "retryable": ... } message. Topics that are all rejected while being authorized still get an empty SUBSCRIBED, but a request refused as a whole (the subscribe rate limit, a malformed topic list, or subscription state that is temporarily unavailable) gets only SUBSCRIBE_REJECTED. Treat each SUBSCRIBE_REJECTED as final for the topics it lists, and resend those marked retryable: true after a short delay, with a cap on attempts. Authentication failed. is marked retryable because a check can fail for a moment, but the server refuses the same credential for 5 seconds and then checks it again, so resending an expired or invalid credential never succeeds: get a fresh one first. Unsubscribe the same way with { "type": "UNSUBSCRIBE", "topics": [...] }. Event messages carry those of their topics that the connection subscribes to, a timestamp, and a data object whose type field identifies the event.
Topics
| Topic | Scope | What it carries |
|---|---|---|
prices:<market_id> | Public | Mark and oracle price updates (PRICE_UPDATED). |
funding:<market_id> | Public | Funding rate updates (FUNDING_RATE_UPDATED). Funding is the periodic payment between longs and shorts that keeps a perpetual's price anchored to the underlying. |
market:<market_id>:orderbook | Public | Aggregated order book level deltas (ORDERBOOK_LEVEL_DELTA), refresh signals (BATCH_ABORTED, ORDER_BOOK_REFRESH), and settled batches (TRADE_BATCH_SETTLED). |
market:<market_id>:trades | Public | One print per settled batch: clearing price, fill count, and total quantity (TRADE_BATCH_SETTLED). |
market:<market_id>:stats | Public | Aggregate market statistics (MARKET_STATS_UPDATED). |
exchange:trading-status | Public | Exchange-wide trading pause state (EXCHANGE_TRADING_PAUSE_UPDATED). |
user:<user_id> | Private | All events for your account across markets: orders, positions, balances, leverage adjustments, deposits, withdrawals, and liquidation notices (see Liquidations). The recommended single subscription for account state. |
deposits:<user_id> | Private | Deposit events only (DEPOSIT_UPDATED). |
orders:<market_id>:<user_id>, positions:<market_id>:<user_id> | Private | Granular per-market slices of your account events. Every user-owned event is also emitted to user:<user_id>, so subscribe to these only when you want narrower streams. |
liquidations:<market_id>:<user_id> | Private | LIQUIDATION, AUTO_DELEVERAGED, and BACKSTOP_TAKEOVER notices for your positions in one market (see Liquidations). The same notices also go to user:<user_id> and positions:<market_id>:<user_id>. |
Authenticating for private topics
Private topics require the same credentials as the REST API (see Authentication & signing), supplied inside the SUBSCRIBE message — either an authorization field with a full header value ("Bearer <token>", "Key <api-key>" or a signed "Token <token>") or an authToken field with a Privy identity token value. UUID session tokens are no longer accepted:
{
"type": "SUBSCRIBE",
"topics": ["user:123"],
"authorization": "Key <api-key>"
}The authenticated user must match the user_id in the topic; otherwise the topic is rejected. Once a connection has authenticated, later SUBSCRIBE messages on the same connection can omit the credential. An Authorization header on the WebSocket connection request does not authenticate subscriptions; send the credential in the SUBSCRIBE message.
When an API key is rotated or revoked, the server closes every connection that key authenticated, with close code 4401 and reason api_key_revoked. Connect again and subscribe with the new key.
A connection authenticated with a signed token closes with code 4401 when its latest token expires, or at the 30-second re-check after its signer can no longer act for the account: the key was revoked, renounced, switched off or reached the end of its lifetime. Send a fresh token in a SUBSCRIBE before the current one expires to keep the connection open (see Authentication & signing).
Event types
Ignore any event type you do not recognize, and any field you do not use. MNX adds event types and fields as the exchange gains features, and a client that stops on an unknown one stops following its orders.
Change: take-profit, stop-loss and reduce-only stop-market orders that fill nothing. A triggered take-profit or stop-loss market order, or a triggered
STOP_MARKETorder placed withreduce_only: true, that a batch fills nothing of is no longer cancelled, neither with reasonmarket_order_no_liquidity(nothing to trade against) nor withmarket_order_ioc(the batch traded without it). It goes back to waiting for its trigger, and you receiveORDER_REARMED. One that a batch fills in part is no longer cancelled for the rest withmarket_order_ioceither: itsORDER_UPDATEDhas statusOPEN, and a later batch fills more of it or sends it back to waiting. When it triggers again you receiveORDER_TRIGGEREDalone, with no secondORDER_CREATED. A client that took either cancellation as the end of the order, or that expects everyORDER_TRIGGEREDto come with anORDER_CREATED, needs to handle these. Also changed: an entry order is cancelled, with reasonstop_loss_triggered, as soon as the mark price reaches its own stop-loss's trigger, even while that stop-loss waits; an entry with nothing filled takes its take-profit and stop-loss with it (parent_order_terminated). A triggered reduce-onlySTOP_MARKETorder also returns to the book when its batch is aborted, and keeps its side when it crosses your own opposite order in a batch.
Your orders, on user:<user_id> and orders:<market_id>:<user_id>:
ORDER_CREATED— an order was accepted, with its id, hash, side, type, price, quantity, remaining quantity, and leverage. A conditional order sends no order event when it is placed. When its trigger fires, itsORDER_CREATEDandORDER_TRIGGEREDarrive together if it passes its checks, orORDER_REJECTEDandORDER_CANCELLEDarrive instead if it fails them (see below). A take-profit or stop-loss market order that triggers again afterORDER_REARMEDsendsORDER_TRIGGEREDalone. An order can also wait, with no event, after the mark price crosses its trigger: a market-type conditional order waits until another account has a limit order on the opposite side within the market's price band, and while its own account has a margin or leverage change in flight; a reduce-only one waits while a settling batch leaves its position nothing to close; a take-profit, stop-loss or reduce-only stop-market order waits while your other triggered ones on that side leave it less than one lot or the market's minimum order size, and for a short rest after going back to waiting; a take-profit or stop-loss leg of an entry order waits while that entry has quantity in a settling batch; and no conditional order triggers while trading is paused, while its market has trading disabled, or while its market is in cancel-only mode. During a guardian halt no conditional order triggers either, and during a bankruptcy hold a market-type one waits (see Placing orders). An entry order's own stop-loss that waits after its trigger is reached still ends the unfilled part of its entry, as when it triggers: the entry getsORDER_CANCELLEDwith reasonstop_loss_triggered.ORDER_TRIGGERED— a conditional order's trigger fired; it is now an open order of its effective market or limit type (see Placing orders for when it can trade). A take-profit or stop-loss market order can trigger more than once (ORDER_REARMED), and so can a reduce-onlySTOP_MARKETorder.ORDER_REARMED— a triggered take-profit or stop-loss market order, or reduce-onlySTOP_MARKETorder, that a batch filled none of went back to waiting for its trigger (PENDING_TRIGGER) instead of being cancelled. It carries the order'sorderId,orderHash,side,originalOrderType,parentOrderHash,protectionScopeandremainingQuantityE18Raw, what its signed quantity has left after the fills it keeps. It triggers again, with its ownORDER_TRIGGERED, once its trigger holds and another account's resting liquidity is there, sized to what it may then close (see Placing orders).ORDER_UPDATED— a batch containing the order settled. It carries the newstatus(OPENwhen part of the order remains,FILLEDwhen none does, orCANCELLEDwhen the batch cancelled what it did not fill, such as the unfilled part of a market order, with the batch'sreason; a triggered take-profit, stop-loss or reduce-only stop-market order keeps that part,OPEN), the quantity filled in that batch (sealedQuantityE18Raw), the remaining quantity, and, when part of the order filled, the fill'sfillPriceE18RawandfillFeeE18Raw. This is the fill event.FILLEDandCANCELLEDend the order. An order the batch fills nothing of gets noORDER_UPDATED: the batch cancels it when it seals, withORDER_CANCELLED, or sends a triggered take-profit, stop-loss or reduce-only stop-market order back to waiting, withORDER_REARMED.ORDER_CANCEL_PENDING_FINALIZE— an order was cancelled while a batch was still settling part of it. The rest of the order is cancelled at once, and the order ends with that batch: withORDER_CANCELLEDand the cancel'sreason, or, when the batch fills the order or cancels it itself, with the batch'sORDER_UPDATED(FILLEDorCANCELLED) and noORDER_CANCELLED. Treat a terminalORDER_UPDATEDas the end of the order.ORDER_CANCELLED— an order was cancelled: by its owner, or by the exchange, whose cancels include an order its batch fills nothing of when the batch seals, for example a market order with no match (a triggered take-profit, stop-loss or reduce-only stop-market order goes back to waiting instead, withORDER_REARMED), an order cancelled to prevent a self-trade (reasonself_trade_prevention), or a market order on the side of its account's liquidation (reasonliquidation_preflight); a market order whose batch was aborted, with reasonbatch_aborted, unless it gets one more try in the next batch and trades there like any market order (see Placing orders), or with a reason naming the failure when the abort blames that order (activated take-profit and stop-loss legs and triggered reduce-only stop-market orders return to the book); and an order it rejected (ORDER_REJECTED). When MNX liquidates an account, it first cancels the account's other orders on that market, untriggered ones included, with reasonliquidation_preflight. Includes areason, and ends the order.ORDER_EXPIRED— an order reached its expiration time and was removed. Carries its id, hash, side, price, quantity, and remaining quantity, and areasonthat holds the order's original type (for exampleLIMIT) rather than a cause. It ends the order. An order that expires while a batch is still settling part of it getsORDER_CANCEL_PENDING_FINALIZEinstead, with thereasonexpired, and ends as described there.ORDER_REJECTED— the exchange rejected an order after accepting its placement: an accepted or triggered order that failed validation when its batch was checked (for example, its margin no longer covered it), a conditional order that failed its checks when its trigger fired (a take-profit, stop-loss or reduce-only stop-market order that would close less than the market's minimum order size gets reasonbelow_minimum_fill), or a reduce-only order, triggered or not, that no longer reduces its position after that position closed or flipped direction (reasonposition_closed; after a flip, reduce-only orders that reduce the new position stay). Untriggered take-profit and stop-loss legs of an entry order that can still fill are the exception: they stay, because later entry fills can open the position again. When the entry ends, each leg it no longer needs getsORDER_CANCELLEDwith noORDER_REJECTED: reasonparent_order_terminatedif nothing filled, orposition_closedif the leg no longer reduces the position. The legs left then follow the position like any other reduce-only order: a later close or flip rejects them as above, even when a leg of the same bracket closed it. Once the bracket's legs have closed as much as its entry filled, the rest getORDER_CANCELLED(reasonbracket_completed), unless the entry had already ended and that close left them nothing to reduce.ORDER_REJECTEDcarries the order's id,reason, andmessage, but not its hash.ORDER_CANCELLEDwith reasonrejected:<reason>follows, or, while a batch is still settling part of the order,ORDER_CANCEL_PENDING_FINALIZEwith that reason and the end described there. A placement that the HTTP API rejects produces no WebSocket event.
To track a conditional order through its whole life, key it by the order_hash returned when you placed it, not by ORDER_CREATED: an order that never triggers, or fails its checks when it does, ends without one. Keep its order_id too: ORDER_TRIGGERED and ORDER_REJECTED carry the id but not the hash. Ids are not the same type everywhere: WebSocket events give orderId as a string, the placement response gives order_id as a number, and order reads such as GET /v0/orders/:order_id give it as a string, so compare ids as strings. If the order ends before it triggers, its last event is ORDER_CANCELLED (preceded by ORDER_REJECTED when the exchange rejected it) or ORDER_EXPIRED. A take-profit, stop-loss or reduce-only stop-market order can wait again after it triggers: ORDER_REARMED puts it back to waiting, and its next ORDER_TRIGGERED carries the same id. An ORDER_CANCELLED whose reason starts with rejected: is final, like any other.
Your account, on user:<user_id>:
POSITION_UPDATED— your position changed on-chain (trade, margin added/removed, leverage adjusted, or liquidation); also onpositions:<market_id>:<user_id>.BALANCE_UPDATED— your margin balance changed.LIQUIDATION— liquidation orders closed some or all of your position in a batch;AUTO_DELEVERAGED— auto-deleveraging reduced or closed your position;BACKSTOP_TAKEOVER— a backstop vault took over your position (see Liquidations). Each is also onpositions:<market_id>:<user_id>andliquidations:<market_id>:<user_id>.DEPOSIT_UPDATEDandWITHDRAW_UPDATED— a deposit or withdrawal was observed on-chain. Withdrawal requests also report their progress withWITHDRAWAL_*andDIRECT_WITHDRAWAL_*events.
Public market data:
PRICE_UPDATED— sent on oracle updates and mark-price ticks.priceandmarkPriceE18Raware the mark price (the oracle price until a market has a mark);oraclePriceandoraclePriceE18Raware the oracle price.FUNDING_RATE_UPDATED— a new funding rate for a market.MARKET_STATS_UPDATED,TRADE_BATCH_SETTLED,ORDERBOOK_LEVEL_DELTA,BATCH_ABORTED,ORDER_BOOK_REFRESH, andEXCHANGE_TRADING_PAUSE_UPDATED— see the topic table and the order book section below.
Liquidations
Liquidation is the forced closing of a position whose margin no longer covers its risk. user:<user_id> receives the ordinary events for the orders, position, and balance involved, and each way of closing the position also sends a notice of its own (below). First, each of your open orders on that market gets ORDER_CANCELLED with reason liquidation_preflight. An order that a batch is still settling gets ORDER_CANCEL_PENDING_FINALIZE with that reason first, and ends as described for that event. What follows depends on the equity left in the position, which is its margin plus unrealized profit or loss and any pending funding (see the three stages in Markets, prices, margin & liquidation):
- Book liquidation: the exchange closes the position with reduce-only market orders (orders that can only shrink the position), sometimes several for one position. Each arrives as
ORDER_CREATEDwithorderTypeMARKET, and itsorderfield, a snapshot of the order, hasoriginal_order_typeLIQUIDATION. Most later events for the order carry no order type, so keep itsorderIdandorderHashto recognize them. It ends like any other market order:ORDER_UPDATEDwhen a batch fills it, which marks that fill withforcedLiquidationtrue, otherwiseORDER_CANCELLED,ORDER_REJECTED, orORDER_EXPIRED(whosereasonisLIQUIDATION), as described above. Unlike other market orders, it is not cancelled withbatch_abortedwhen its batch is aborted: it waits for the next batch, unless the abort blames it for the failure, which cancels it. Each batch that fills your liquidation orders also sends oneLIQUIDATIONnotice. - Backstop: a backstop vault takes over the position, either straight away or after liquidation orders have shrunk it. You get
POSITION_UPDATEDwithsize"0",usedMargin"0", andaction5, which marks a backstop takeover (a trade is0). The takeover itself sends noBALANCE_UPDATED: the position's margin goes to the vault with it. It sends oneBACKSTOP_TAKEOVERnotice. - No equity left: no liquidation order or takeover. Auto-deleveraging (ADL) closes the position at its bankruptcy price, the price at which its equity is zero, against positions on the other side of the market, and sends
AUTO_DELEVERAGEDwithroleliquidated. If a backstop vault that took over a position runs out of equity, ADL closes the vault's position instead, which sends the trader it came from nothing more.
In each case, POSITION_UPDATED reports the position as it shrinks or closes, and BALANCE_UPDATED reports any change to your margin balance. When a takeover or auto-deleveraging closes a position, any reduce-only orders its account still has on that market get ORDER_REJECTED with reason position_closed, followed by the cancellation described for that event (reason rejected:position_closed), except the entry legs described there.
LIQUIDATION reports a book liquidation once it has settled: one notice per batch that filled your liquidation orders, sent only to you, on user:<user_id>, positions:<market_id>:<user_id>, and liquidations:<market_id>:<user_id>. A connection subscribed to more than one of them receives it once. It carries batchId; priceE18Raw, the batch's clearing price, at which every liquidation fill executed; quantityE18Raw, the unsigned quantity the batch closed; positionSizeE18Raw, your signed position size when the liquidation was checked (negative for a short); balanceDeltaE18Raw, the signed change to your balance (the margin released, net of the loss and fees); marginRatioE18Raw and maintenanceMarginRatioE18Raw, your margin ratio at the check and the maintenance ratio it fell below; and settledAt. A batch that lands on chain after the exchange had given it up sends no LIQUIDATION notice; POSITION_UPDATED and BALANCE_UPDATED still report it.
AUTO_DELEVERAGED reports an applied ADL transaction: one notice for each account whose position it changed, sent only to that account, on the same three topics. role is counterparty when your position on the other side of a bankrupt one was reduced or closed to cover it, or liquidated when your own position, out of equity, was closed. It carries your own part only; it doesn't name the bankrupt account or show anyone else's position: side, your position's side (LONG or SHORT, or null when the event cannot tell, as when every position the transaction touched ended closed); quantityE18Raw, the unsigned quantity it closed; priceE18Raw, the bankrupt position's bankruptcy price, at which every part executed (weighted by quantity and rounded down when there were several), which in a large price gap can be worse than your own entry price; remainingSizeE18Raw, your signed position size afterwards ("0" when closed, null when the event cannot tell); realizedPnlE18Raw, the signed profit or loss realized at that price; balanceDeltaE18Raw, the signed change to your balance (the margin released plus that profit or loss); txHash; and settledAt.
BACKSTOP_TAKEOVER reports that a backstop vault took over your whole position with its margin: one notice per takeover, on the same three topics. It carries side, quantityE18Raw, priceE18Raw (the oracle price the vault took the position at), and txHash, each null when the exchange has not seen the vault's side of the takeover with it; marginForfeitedE18Raw, the margin that went to the vault; realizedPnlE18Raw, minus that margin; and settledAt.
No notice is sent again after a reconnect: GET /v0/account/transactions?transaction_type=liquidation lists book liquidations, auto-deleveraging, and takeovers, with the side, quantity, price, realized profit or loss, and the margin a takeover forfeited.
BALANCE_UPDATED goes only to user:<user_id>. An order that fills against a liquidation order gets an ordinary ORDER_UPDATED, not marked as part of a liquidation. A position that ADL reduces or closes on the other side of a bankrupt one gets POSITION_UPDATED, BALANCE_UPDATED when its balance changes, and AUTO_DELEVERAGED with role counterparty.
Units
WebSocket numbers are not all in one unit. Fields ending in E18Raw or Raw are exact 1e18-scaled integers sent as strings. Most other numeric fields, such as price, quantity, and remainingQuantity, are plain decimal numbers for display. A few raw strings have no suffix: size and usedMargin in POSITION_UPDATED, and fundingRate in FUNDING_RATE_UPDATED. Do exact math on the raw strings (see Getting started).
Keeping an order book in sync
To mirror a market's order book, combine REST snapshots with additive deltas:
- Subscribe to
market:<market_id>:orderbookand wait forSUBSCRIBED. Keep itssubscription_read_after. - Fetch a snapshot from
GET /v0/markets/:market_id/book, passing that floor asread_after_shard_idandread_after_shard_seqso the snapshot includes everything before your subscription. A snapshot holds the levels inside the market's take band, which it reports astake_band(null, with both sides empty, while the market has no positive oracle price): 50 per side by default, anddepthrequests up to 1,000. The response'sdepthis the depth actually applied, and the snapshot carries its ownread_after. While the server's book is not ready, for example during a restart, the route answers 503 withRetry-After. With your bookmark it can also answer 503 withdetails.codeHTV1_READ_AFTER_PENDING, or 429 withHTV1_READ_AFTER_CAPACITY. In each case keep the same bookmark and retry after theRetry-Afterdelay. A bookmark the server cannot place answers 409 withHTV1_READ_AFTER_STALE_BOOKMARKinstead of waiting it out: it names another owner, its sequence is not a whole number, or the exchange has not reached that sequence. Drop it and fetch the snapshot again without it. - Apply
ORDERBOOK_LEVEL_DELTAevents additively on top, skipping deltas whose price is outside the snapshot'stake_band. Each delta carries adeltaIdfor duplicate-delivery protection and areadAftercoordinate; a snapshot already includes all deltas at or below its sequence, so skip deltas at or below the snapshot'sread_after. A batch's matched size leaves a level when the batch is sealed, and settlement changes nothing. - Buffer deltas that arrive while the snapshot loads, and apply the ones above its
read_afteronce it lands. Compare each side with the response'sdepth, not the one you sent. If a side came back full (as many levels withoutbeyond_depthasdepth), apply deltas only to prices up to its last such level and to its flagged levels, and refetch the snapshot when removals thin that side; a bounded snapshot cannot tell you which levels sit behind it. A side with fewer levels thandepthis the whole side within the band, so apply every in-band delta to it. - Each level's
tradeablesize is the part of it that counts toward prices at the time of the snapshot: what the matcher would consider, less the orders that have not rested yet. Reduce-only orders count only what they can close: an account's reduce-only orders on a side share its position, best price first, after its ordinary orders on that side at the same or a better price, and the take-profit and stop-loss legs of an entry order share what the entry has filled. A buyer or seller that also reaches that account's ordinary orders at worse prices gets those first, so it can get less of the reduce-only orders than the snapshot shows, by at most the account's position. An order rests once it was live 400 ms before one of the market's batches sealed, or 400 ms passed with none of them settling. Until then it is in the level'squantityand the next batch can match it, but it is not in the tradeable size or the best bid and ask.next_rest_in_msis how long after the snapshot the next such order counts andlast_rest_in_mshow long until the last of them does (an order you just placed is the newest), or null when none will without a new event (none is left out, or one of the market's batches is settling). Deltas do not carry them: once a delta touches a level, show itsquantityand refetch the snapshot now and then (at most every few seconds) to refresh it. When some of the firstdepthlevels have no tradeable size, the deeper levels that do come after them, flaggedbeyond_depth, untildepthlevels with tradeable size are in or the band ends (at most twicedepthlevels per side); do not trim them. - On
BATCH_ABORTED— the coarse refresh signal for paths that cannot be expressed as a precise level delta — refetch the snapshot (using the event'sreadAfter). - On
ORDER_BOOK_REFRESH, refetch the snapshot the same way. A market in its opening auction has a sealed book: its snapshot shows no levels and its topic sends no level deltas and no batch aborts until it opens. The event comes when the auction closes, its held result is accepted, or its result is planned again after orders are cancelled (reasonauction_closed; the snapshot then carries the auction's result), when the auction is extended (extended; the snapshot is back in its pre-open) and when the market opens (opened; the snapshot then shows the book).
Connection lifecycle
- The server sends protocol-level WebSocket ping frames every 30 seconds and drops connections that stop answering. Standard WebSocket clients answer these automatically; no application code is needed. An application-level
{ "type": "PING" }/{ "type": "PONG" }exchange is also available for testing. - Before the server shuts down, for a deploy or any other restart, it sends a
SERVER_RESTARTINGmessage and closes with WebSocket close code 1012. Reconnect and re-send your subscriptions — they are not persisted across connections. - A
SUBSCRIBEmay list at most 256 topics, and a connection may hold at most 1,024 topics and send 30SUBSCRIBEmessages per 10 seconds; past that, requests getSUBSCRIBE_REJECTED. The server closes a connection with code 1009 for a frame over 16 KiB, with 1008 for more than 120 messages or 1 MiB a minute from the client (your own pings count), and with 4002 when more than 512,000 bytes of events wait because the client reads too slowly. ASUBSCRIBEwith another user's credential also closes the connection (1008): open a new connection to switch accounts. Rotating or revoking the API key that authenticated a connection closes it with 4401 (reasonapi_key_revoked). - Events published while you are disconnected are not replayed. After every reconnect, wait for the new
SUBSCRIBEDand refetch the state you track (orders, positions, balances, order books) from itssubscription_read_after, as in the order book steps above.