Files
eth_hedge_sim/backend/app/strategy/open_capacity.py
T

213 lines
7.0 KiB
Python

"""开仓资金可开判定:只看交易账户(永续 USDT / 期权 USDC)。"""
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
# 不足期间只推一次;资金恢复后清零,下次再不足可再推一次
_notified_while_short: bool = False
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] = {
"trading_usdt": None,
"trading_usdc": None,
}
try:
from ..live.okx_funds import OkxFundsClient
client = OkxFundsClient()
try:
bal = client.fetch_balances()
out["trading_usdt"] = _f(bal.get("trading_usdt"))
out["trading_usdc"] = _f(bal.get("trading_usdc"))
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).view()
return {
"perp_usdt": float(w.get("trading_usdt") or 0),
"option_usdc": float(w.get("trading_usdc") or 0),
}
def assess_open_capacity(db: Database | None = None) -> dict[str, Any]:
"""
返回永续/期权是否有足够交易账户资金开新仓。
- 永续:交易账户 USDT >= 名义/杠杆
- 期权:交易账户 USDC >= 卖一×名义×(1+费率)
"""
global _notified_while_short
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_usdt = live.get("trading_usdt")
t_usdc = live.get("trading_usdc")
have_perp = float(t_usdt) if t_usdt is not None else None
have_opt = float(t_usdc) if t_usdc is not None else None
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
funds_ok = perp_ok is True and opt_ok is True
# 资金恢复后允许下次不足再通知一次
if funds_ok:
_notified_while_short = False
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": funds_ok,
"source": "trading",
}
def maybe_notify_funds_short(cap: dict[str, Any] | None = None) -> None:
"""仅在「不能开」时推送一次;能开绝不通知。"""
global _notified_while_short
cap = cap or assess_open_capacity()
# 能开 / 未知:不通知;资金恢复则重置,便于下次不足再提醒一次
if cap.get("funds_ok") is True:
_notified_while_short = False
return
cannot = cap.get("perp_can_open") is False or cap.get("option_can_open") is False
if not cannot:
return
if _notified_while_short:
return
_notified_while_short = True
parts: list[str] = []
if cap.get("perp_can_open") is False:
parts.append(
f"交易账户 USDT 不足:需约 {cap.get('perp_need_usdt')}U,现有 {cap.get('perp_have_usdt')}U"
)
if cap.get("option_can_open") is False:
parts.append(
f"交易账户 USDC 不足:需约 {cap.get('option_need_usdc')}U,现有 {cap.get('option_have_usdc')}U"
)
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")