"""LIVE 开仓对账:占槽、交易所持仓核对、平永续数量解析。""" from __future__ import annotations import logging from typing import Any from ..config import get_settings from ..exchange.runtime import load_runtime_settings from ..sim.matcher import BLOCKING_STATUSES from .symbols import resolve_perp_inst_id logger = logging.getLogger(__name__) _PERP_EPS = 1e-8 def claim_open_slot(db) -> tuple[bool, str]: """原子占用开仓槽:flat/空 → opening;已有 blocking 状态则拒绝。""" with db._lock: row = db._conn.execute("SELECT status FROM positions WHERE id=1").fetchone() st = str(row["status"] or "") if row else "" if st in BLOCKING_STATUSES: return False, f"已有持仓/半仓状态({st}),请先修复或平仓" cur = db._conn.execute( """UPDATE positions SET status='opening' WHERE id=1 AND (status IS NULL OR status='' OR status='flat')""" ) if cur.rowcount != 1: st2 = st or "unknown" return False, f"无法占用开仓槽(当前 status={st2})" db._conn.commit() return True, "ok" def release_open_slot_if_opening(db) -> None: """开仓失败且未落 half_open/open 时,释放 opening 占槽。""" with db._lock: db._conn.execute( "UPDATE positions SET status='flat' WHERE id=1 AND status='opening'" ) db._conn.commit() def exchange_perp_abs_size( client: Any, exchange: str, perp_inst_id: str, perp_side: str, ) -> float | None: """查询交易所永续绝对持仓:OKX 张数,Binance ETH。""" ex = (exchange or "").strip().lower() try: if ex == "binance": ps = "LONG" if perp_side == "long" else "SHORT" return client.get_perp_pos_sz(perp_inst_id, position_side=ps) ps = "long" if perp_side == "long" else "short" return client.get_perp_pos_sz(perp_inst_id, pos_side=ps) except Exception as e: logger.warning("exchange_perp_abs_size failed exchange=%s: %s", ex, e) return None def assert_safe_to_open_live(executor) -> tuple[bool, str]: """LIVE 开仓前:本地无 blocking 仓,且交易所无残留永续(flat/opening 时)。""" if get_settings().is_sim: return True, "ok" pos = executor.current_position() st = str(pos.get("status") or "") if st in BLOCKING_STATUSES and st != "opening": return False, f"已有持仓/半仓状态({st}),请先修复或平仓" client, ex_name = _executor_client_and_exchange(executor) if client is None: return False, "无法核对交易所持仓" perp_inst = resolve_perp_inst_id(executor.db) # flat/opening:两侧都查,避免只查默认 long 漏掉 short 残留 if st in ("flat", "", "opening"): sides = ("long", "short") else: sides = (str(pos.get("perp_side") or "long"),) total = 0.0 for side in sides: ex_sz = exchange_perp_abs_size(client, ex_name or "", perp_inst, side) if ex_sz is None: return False, "无法核对交易所持仓" total += float(ex_sz) if total > _PERP_EPS and st in ("flat", "", "opening"): return ( False, "交易所有永续仓但本地无持仓,禁止新开,请人工核对", ) return True, "ok" def log_exchange_db_mismatch(executor) -> None: """LIVE 启动时记录交易所 vs 本地持仓不一致(仅日志,不阻断)。""" if get_settings().is_sim: return ok, msg = assert_safe_to_open_live(executor) if ok: logger.info("LIVE startup reconcile: exchange/DB perp OK") else: logger.warning("LIVE startup reconcile mismatch: %s", msg) def perp_close_contracts_okx( client: Any, *, perp_inst: str, perp_side: str, perp_qty_eth: float, ct_val: float, ) -> int: """平永续张数:优先交易所持仓,否则 DB qty/ct_val。""" ps = "long" if perp_side == "long" else "short" ex_sz = client.get_perp_pos_sz(perp_inst, pos_side=ps) if ex_sz is not None and ex_sz > _PERP_EPS: return max(1, int(round(ex_sz))) return max(1, int(round(perp_qty_eth / ct_val))) def perp_close_qty_eth_binance( client: Any, *, perp_inst: str, perp_side: str, perp_qty_eth: float, ) -> float: """平永续 ETH 数量:优先交易所持仓,否则 DB qty。""" ps = "LONG" if perp_side == "long" else "SHORT" ex_sz = client.get_perp_pos_sz(perp_inst, position_side=ps) if ex_sz is not None and ex_sz > _PERP_EPS: return float(ex_sz) return float(perp_qty_eth) def _executor_client_and_exchange(executor) -> tuple[Any | None, str | None]: if get_settings().is_sim: return None, None try: ex_name = load_runtime_settings().exchange except Exception: ex_name = get_settings().exchange client = None if hasattr(executor, "_client"): try: client = executor._client() except Exception as e: logger.warning("live executor client unavailable: %s", e) return None, ex_name return client, ex_name