"""开仓资金可开判定:永续保证金 + 期权权利金。""" from __future__ import annotations import logging import time from typing import Any from ..config import get_settings from ..models.db import Database, get_db from ..sim.funds_wallets import SimFundsWallets from ..sim.ledger import Ledger logger = logging.getLogger(__name__) _live_bal_cache: dict[str, Any] = {"ts": 0.0, "data": None} _LIVE_BAL_TTL_SEC = 8.0 _last_notify_key: str | None = None _last_notify_ms: float = 0.0 _NOTIFY_DEDUP_SEC = 600.0 # 同状态 10 分钟内不重复推 def _f(v: Any) -> float | None: try: if v is None or v == "": return None return float(v) except (TypeError, ValueError): return None def _index_and_option_ask() -> tuple[float | None, float | None]: """指数价 + 期权卖一粗估(取 Call/Put 卖一较大者,偏保守)。""" try: from .session import get_session snap = get_session().snapshot() except Exception: return None, None idx = _f(getattr(snap, "index_px", None)) if idx is None and snap.perp: idx = _f(snap.perp.mark_px) or _f(snap.perp.ask) or _f(snap.perp.bid) asks: list[float] = [] for leg in (snap.call, snap.put): if leg is None: continue a = _f(leg.ask) if a is not None and a > 0: asks.append(a) ask = max(asks) if asks else None return idx, ask def _live_balances() -> dict[str, float | None]: now = time.time() if _live_bal_cache["data"] is not None and now - float(_live_bal_cache["ts"]) < _LIVE_BAL_TTL_SEC: return dict(_live_bal_cache["data"]) out: dict[str, float | None] = { "funding_usdt": None, "trading_usdt": None, "options_funding_usdc": None, "options_trading_usdc": None, } try: from ..live.okx_funds import OkxFundsClient client = OkxFundsClient() try: bal = client.fetch_balances() for k in out: out[k] = _f(bal.get(k)) finally: client.close() except Exception as e: logger.warning("open_capacity live balance failed: %s", e) _live_bal_cache["ts"] = now _live_bal_cache["data"] = dict(out) return out def _sim_balances(db: Database) -> dict[str, float]: w = SimFundsWallets(db).snapshot() led = Ledger(db).snapshot() avail = float(led.get("available") or 0) funding = float(w.get("funding_usdt") or 0) trading = float(w.get("trading_usdt") or 0) opt_f = float(w.get("options_funding_usdc") or 0) opt_t = float(w.get("options_trading_usdc") or 0) # 钱包未播种时回退账本可用 usdt = funding + trading if usdt < 1e-9: usdt = avail usdc = opt_f + opt_t if usdc < 1e-9: # SIM 早期权利金从账本扣;无 USDC 钱包时用可用资金估期权可开 usdc = avail return { "perp_usdt": usdt, "option_usdc": usdc, "ledger_available": avail, } def assess_open_capacity(db: Database | None = None) -> dict[str, Any]: """ 返回永续/期权是否有足够资金开新仓。 - 永续:需 USDT >= 名义/杠杆 - 期权:需 USDC(或 SIM 回退可用) >= 卖一×名义×(1+费率) """ db = db or get_db() s = get_settings() ledger = Ledger(db) lev = float(ledger.get_setting_float("leverage", s.leverage) or 3) if lev <= 0: lev = 3.0 perp_qty = float(ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth) or 1) opt_qty = float(ledger.get_setting_float("option_qty_eth", s.option_qty_eth) or 2) fee_rate = float(ledger.get_setting_float("fee_rate", s.fee_rate) or 0.0005) idx, ask = _index_and_option_ask() margin_need = (float(idx) * perp_qty / lev) if idx and idx > 0 else None premium_need = ( float(ask) * opt_qty * (1.0 + fee_rate) if ask is not None and ask > 0 else None ) if s.is_sim: bal = _sim_balances(db) have_perp = float(bal["perp_usdt"]) have_opt = float(bal["option_usdc"]) else: live = _live_balances() # 永续用交易账户;若为空则合并资金账户(提示需划转但仍可显示) t = live.get("trading_usdt") f = live.get("funding_usdt") if t is not None and t > 1e-9: have_perp = float(t) elif t is not None or f is not None: have_perp = float(t or 0) + float(f or 0) else: have_perp = None of_ = live.get("options_funding_usdc") ot_ = live.get("options_trading_usdc") if of_ is None and ot_ is None: have_opt = None else: have_opt = float(of_ or 0) + float(ot_ or 0) perp_ok: bool | None if margin_need is None or have_perp is None: perp_ok = None else: perp_ok = have_perp + 1e-9 >= margin_need opt_ok: bool | None if premium_need is None or have_opt is None: opt_ok = None else: opt_ok = have_opt + 1e-9 >= premium_need lev_i = int(round(lev)) if abs(lev - round(lev)) < 1e-9 else lev if perp_ok is True: perp_label = f"永续{lev_i}x 可开" elif perp_ok is False: perp_label = f"永续{lev_i}x 不可开" else: perp_label = f"永续{lev_i}x —" if opt_ok is True: opt_label = "期权可开" elif opt_ok is False: opt_label = "期权不可开" else: opt_label = "期权 —" return { "leverage": lev, "perp_qty_eth": perp_qty, "option_qty_eth": opt_qty, "index_px": idx, "option_ask": ask, "perp_need_usdt": round(margin_need, 2) if margin_need is not None else None, "option_need_usdc": round(premium_need, 2) if premium_need is not None else None, "perp_have_usdt": round(have_perp, 2) if have_perp is not None else None, "option_have_usdc": round(have_opt, 2) if have_opt is not None else None, "perp_can_open": perp_ok, "option_can_open": opt_ok, "perp_label": perp_label, "option_label": opt_label, "funds_ok": (perp_ok is True and opt_ok is True), } def maybe_notify_funds_short(cap: dict[str, Any] | None = None) -> None: """资金不足时企业微信推送(去重)。""" global _last_notify_key, _last_notify_ms cap = cap or assess_open_capacity() parts: list[str] = [] if cap.get("perp_can_open") is False: parts.append( f"永续不足:需约 {cap.get('perp_need_usdt')}U,现有 {cap.get('perp_have_usdt')}U" ) if cap.get("option_can_open") is False: parts.append( f"期权不足:需约 {cap.get('option_need_usdc')}U,现有 {cap.get('option_have_usdc')}U" ) if not parts: return key = "|".join(parts) now = time.time() if key == _last_notify_key and now - _last_notify_ms < _NOTIFY_DEDUP_SEC: return _last_notify_key = key _last_notify_ms = now try: from ..notify import wecom wecom.notify_async( wecom.build_markdown( tag=wecom.TAG_FAULT, title="资金不足 · 无法开新仓", lines=[ f"**永续**: {cap.get('perp_label')}", f"**期权**: {cap.get('option_label')}", *[f"**详情**: {p}" for p in parts], "请划转/兑换后重试。", ], ) ) except Exception: logger.exception("wecom funds notify failed")