vike
ConceptsReconciliation

Reconciliation

How a live mount proves its own books against the venue's own reports — the pass, the health gate, and which venues reconcile.

A live core's books are built from messages. Messages get lost, arrive while the process is down, or arrive twice. Reconciliation is the periodic proof that the local book still matches what the venue itself says — and, where it does not, a classified disagreement with a policy attached.

The engine is pure. crates/vike-exec/src/recon/mod.rs's header states the split: diff turns venue reports plus a LocalView into Divergences, resolve turns those plus a ReconPolicy into a Recon, and both are pure functions with no I/O. That is what makes golden-fixture tests and the three-way journal cross-check possible at all — crates/vike-exec/src/recon/types.rs says so in its own first line.

What one pass fetches

crates/vike-exec/src/recon/client.rs's ReconClient is the venue-facing report seam, and it is blocking REST. Three fetches are required — fetch_order_status_reports, fetch_fill_reports, fetch_position_status_reports. Two more default to Ok(None), so an unwired client stays inert rather than failing a pass:

  • fetch_balance — the venue's absolute cash truth. A venue that reports none still has its orders, fills and positions diffed.
  • fetch_fee_rates — the account-actual maker/taker schedule, overridden by binance, bybit, okx, deribit and aster — the last on its spot lane only, its perp lane staying inert. Unwired, the caller keeps the static vike_model::fee_schedule_for default.

fetch_mass_status composes the three required fetches into one MassStatus bundle in a fixed order (orders → fills → positions); a venue with a genuine atomic bulk endpoint may override it, and that doc records that no current venue does. run_pass composes fetch → diff → resolve in one function — the offline twin the golden fixtures run. The live driver reaches the same three stages across two threads instead, which the next section describes.

Where the work runs

crates/vike-core/src/recon_manager.rs's header names the design reports-in, compute-on-fold. The manager runs on its own thread and does only the blocking fetch — it must never touch the p99 < 10 µs fold. It then enqueues the raw reports as Command::ReconcileReports; the fold thread reads its own local_view(), runs the pure diff and resolve, and folds each synthesized event through the same publish_to path real venue events take (crates/vike-core/src/runtime/mod.rs's reconcile_reports). Diff and resolve therefore sit on the same thread as the state they read: no cross-thread snapshot, no staleness window.

The pass deliberately avoids ApplySnapshot

reconcile_reports explains why it does not route through Command::ApplySnapshot: ExecutionEngine::apply_snapshot's order reap terminalizes any live local order absent from snapshot.open_orders, so pushing an all-empty-except-balance snapshot through that arm would cancel every live order on the venue as a side effect. Balance is seeded directly onto Account::balance instead — the narrowest path that updates cash without touching positions or the order registry.

Cadence, and why repetition is safe

Three cadences share one pass (ReconManager::run_startup_pass): a startup pass after startup_delay; an on-demand pass whenever a bridge pokes the reconcile trigger, typically wired to a reconnect; and ReconConfig::interval, which re-runs the same pass on a fixed cadence. ReconConfig::audit_interval is a fourth arm that runs no reconcile pass at all — it pokes the core's existing stuck-order watchdog waker instead.

That makes idempotency load-bearing rather than convenient, and the header says where it comes from: diff dedups fills by trade_id against the engine's seen_trade_ids, so a tick that finds nothing new folds nothing new.

The health gate is per venue

ReconManager::should_reconcile is consulted before each venue's leg of every pass — startup, trigger and interval alike. With no probe wired it returns true; otherwise it asks the probe for that venue and suppresses the leg on ReconHealth::Degraded. Suppression defers that venue only: it sets no pending flag and never blocks a different healthy venue in the same pass.

The probe itself is built in crates/vike-ops/src/reconcile_config.rs's build_recon_config over a per-venue map of feed-status handles, so a bybit feed gap suppresses only bybit's leg. A venue with no entry in the map reads Healthy — an ungated venue is never blocked, which is how the exec-only venues (no market feed of their own) keep reconciling. health_from_feed_status is fail-soft: only an error state, or a disconnected one whose status string literally says so, reads Degraded.

Which venues reconcile

The mount is the composition root, and crates/vike-mount/src/lib.rs's header describes a layout of three kinds, not two:

  • build_recon_client is a thin dispatch over the bybit, okx and binance bridges' own recon_client factories.
  • Hyperliquid is built by crates/vike-mount/src/hyperliquid.rs's hyperliquid_live_client, which returns the exec client and the ReconClient together — it calls the bridge's recon_client only once its instruments fetch has resolved the product, which the shared dispatch cannot do without a second network round trip.
  • deribit, aster, cTrader, Alpaca, IG, OANDA — and, behind the ibkr and polymarket features, IBKR and Polymarket — call their own factory inline in make_engine's arm, each needing a non-REST, bespoke-signer or non-generic-credentials handshake the shared dispatch cannot produce.

A paper venue returns None and reconciles nothing, and there are two independent ways to be one. The per-venue arming ceiling (policy.venues.<venue>) is read first, above the credential read: a paper venue returns the paper client having loaded no credential and opened no socket. Only behind it does the older rule apply — absent credentials are the live gate. Both, in that order, are the risk and arming page's subject.

Five of those inline factories (deribit, cTrader, IG, OANDA, IBKR) perform a blocking authenticated handshake just to construct the client, so the mount gates them on its resolved reconcile verdict through recon_if_enabled: with reconciliation off they are never called and the venue does no authenticated network work at mount. That gate is an explicit parameter and deliberately not "does this venue have a reconnect trigger" — the trigger is wired for four venues only, so inferring the global gate from it would silently stop reconciling those five.

The output an operator sees

Events the policy auto-applies fold immediately. Everything held becomes a ReconAlert carrying proposed_events — the events resolve would have folded — that an operator confirm folds later through the same publish path, byte-identically to what an immediate fold would have produced (confirm_recon). Which is which is the policies page's subject, and each kind has a page.

A held divergence is a state, not an event

crates/vike-core/src/runtime/recon_held.rs's header carries the measurement that forced this: under a hold-everything policy a divergence nothing heals is re-held every interval forever, and the live box logged 396 identical warnings on 2026-08-25 and 345 on 2026-08-24, every one of them the same bybit external-position divergence. The log now announces transitions — one line when a divergence enters the held set, one when it leaves — with a much slower periodic summary so a standing backlog stays visible without being repeated at pass cadence. The held store has the same shape: a re-raise refreshes its row in place, so it is bounded by distinct divergence identities rather than by uptime.

On this page