from __future__ import annotations from dataclasses import asdict from typing import Any from crypto_spot_bot.bybit import BybitClient, Instrument from crypto_spot_bot.config import Settings from crypto_spot_bot.storage import Storage def reconciliation_snapshot( *, settings: Settings, storage: Storage, client: BybitClient, instruments: dict[str, Instrument], ) -> dict[str, Any]: local_positions = storage.open_positions() local = [ { "id": position.id, "symbol": position.symbol, "qty": position.qty, "entry_price": position.entry_price, "notional_usdt": position.notional_usdt, } for position in local_positions ] if not settings.live_ready: return { "status": "paper", "live_ready": False, "local_positions": local, "remote_balances": [], "open_orders": [], "discrepancies": [], "message": "reconciliation requires unlocked live Bybit credentials", } try: coins = _coins_for_symbols(settings.symbols, instruments) wallet = client.wallet_balance(coin=",".join(coins)) orders = client.realtime_orders(category="spot", open_only=0, limit=50) except Exception as exc: return { "status": "error", "live_ready": True, "local_positions": local, "remote_balances": [], "open_orders": [], "discrepancies": [{"severity": "error", "code": "bybit_read_failed", "message": str(exc)}], } balances = _balances(wallet) open_orders = orders.get("list", []) if isinstance(orders.get("list"), list) else [] discrepancies = _discrepancies(local_positions, balances, instruments) return { "status": "warn" if discrepancies else "ok", "live_ready": True, "local_positions": local, "remote_balances": balances, "open_orders": open_orders, "discrepancies": discrepancies, "account": _account_summary(wallet), "instruments": {symbol: asdict(info) for symbol, info in instruments.items() if symbol in settings.symbols}, } def _coins_for_symbols(symbols: tuple[str, ...], instruments: dict[str, Instrument]) -> list[str]: coins = {"USDT"} for symbol in symbols: info = instruments.get(symbol) if info and info.base_coin: coins.add(info.base_coin.upper()) elif symbol.endswith("USDT"): coins.add(symbol[:-4].upper()) return sorted(coins) def _balances(wallet: dict[str, Any]) -> list[dict[str, Any]]: rows = wallet.get("list") if not isinstance(rows, list) or not rows: return [] coins = rows[0].get("coin") if not isinstance(coins, list): return [] output = [] for row in coins: if not isinstance(row, dict): continue output.append( { "coin": str(row.get("coin", "")), "equity": _float(row.get("equity")), "wallet_balance": _float(row.get("walletBalance")), "locked": _float(row.get("locked")), "usd_value": _float(row.get("usdValue")), } ) return output def _account_summary(wallet: dict[str, Any]) -> dict[str, Any]: rows = wallet.get("list") if not isinstance(rows, list) or not rows: return {} row = rows[0] return { "account_type": row.get("accountType"), "total_equity": _float(row.get("totalEquity")), "total_wallet_balance": _float(row.get("totalWalletBalance")), "total_available_balance": _float(row.get("totalAvailableBalance")), } def _discrepancies( positions: list[Any], balances: list[dict[str, Any]], instruments: dict[str, Instrument], ) -> list[dict[str, Any]]: by_coin = {str(row.get("coin", "")).upper(): row for row in balances} local_by_coin: dict[str, float] = {} for position in positions: info = instruments.get(position.symbol) coin = info.base_coin.upper() if info and info.base_coin else position.symbol.removesuffix("USDT") local_by_coin[coin] = local_by_coin.get(coin, 0.0) + float(position.qty) issues = [] for coin, local_qty in local_by_coin.items(): remote_qty = _float((by_coin.get(coin) or {}).get("equity")) tolerance = max(1e-8, local_qty * 0.002) if remote_qty + tolerance < local_qty: issues.append( { "severity": "error", "code": "remote_balance_below_local_position", "coin": coin, "local_qty": round(local_qty, 10), "remote_qty": round(remote_qty, 10), } ) for coin, row in by_coin.items(): if coin == "USDT": continue remote_qty = _float(row.get("equity")) local_qty = local_by_coin.get(coin, 0.0) if remote_qty > max(1e-8, local_qty * 1.05) and local_qty <= 0: issues.append( { "severity": "warn", "code": "remote_asset_without_local_position", "coin": coin, "remote_qty": round(remote_qty, 10), } ) return issues def _float(value: Any) -> float: try: return float(value) except (TypeError, ValueError): return 0.0