8d67f3fc6c
Co-authored-by: Cursor <cursoragent@cursor.com>
323 lines
11 KiB
Python
323 lines
11 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 invalidate_live_balance_cache() -> None:
|
||
"""兑换/划转后强制下次重拉交易账户余额。"""
|
||
_live_bal_cache["ts"] = 0.0
|
||
_live_bal_cache["data"] = None
|
||
|
||
|
||
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 ..exchange.runtime import load_runtime_settings
|
||
|
||
ex = str(load_runtime_settings().exchange or "").strip().lower()
|
||
if ex in ("binance", "bn"):
|
||
from ..live.binance_trade import BinanceTradeClient
|
||
|
||
client = BinanceTradeClient()
|
||
try:
|
||
bal = client.fetch_balances()
|
||
out["trading_usdt"] = _f(bal.get("trading_usdt"))
|
||
out["trading_usdc"] = _f(bal.get("trading_usdc"))
|
||
finally:
|
||
client.close()
|
||
else:
|
||
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,
|
||
*,
|
||
option_ask: float | None = None,
|
||
option_qty_eth: float | None = None,
|
||
perp_qty_eth: float | None = None,
|
||
call_ask: float | None = None,
|
||
put_ask: float | None = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
返回永续/期权是否有足够交易账户资金开新仓。
|
||
- 永续:交易账户 USDT >= 名义/杠杆
|
||
- 期权:交易账户 USDC >= 卖一×名义×(1+费率)
|
||
- 期期:期权需 (call_ask+put_ask)×qty×(1+fee);永续视为不需要
|
||
"""
|
||
global _notified_while_short
|
||
db = db or get_db()
|
||
s = get_settings()
|
||
ledger = Ledger(db)
|
||
hedge = str(
|
||
ledger.get_setting_str("hedge_mode", s.hedge_mode) or s.hedge_mode
|
||
).strip().lower()
|
||
if hedge not in ("perp_option", "option_option"):
|
||
hedge = "perp_option"
|
||
lev = float(ledger.get_setting_float("leverage", s.leverage) or 3)
|
||
if lev <= 0:
|
||
lev = 3.0
|
||
perp_qty = float(
|
||
perp_qty_eth
|
||
if perp_qty_eth is not None
|
||
else (ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth) or 1)
|
||
)
|
||
opt_qty = float(
|
||
option_qty_eth
|
||
if option_qty_eth is not None
|
||
else (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_book = _index_and_option_ask()
|
||
ask = float(option_ask) if option_ask is not None and float(option_ask) > 0 else ask_book
|
||
if hedge == "option_option":
|
||
ca = float(call_ask) if call_ask is not None and float(call_ask) > 0 else None
|
||
pa = float(put_ask) if put_ask is not None and float(put_ask) > 0 else None
|
||
if ca is None or pa is None:
|
||
# 回退:用监控对 call/put 卖一
|
||
try:
|
||
from .session import get_session
|
||
|
||
snap = get_session().snapshot()
|
||
if ca is None and snap.call and snap.call.ask:
|
||
ca = float(snap.call.ask)
|
||
if pa is None and snap.put and snap.put.ask:
|
||
pa = float(snap.put.ask)
|
||
except Exception:
|
||
pass
|
||
if ca is not None and pa is not None and ca > 0 and pa > 0:
|
||
call_q = float(opt_qty)
|
||
put_q = float(
|
||
ledger.get_setting_float("oo_put_qty_eth", call_q) or call_q
|
||
)
|
||
# 与定仓一致:两腿各自权利金
|
||
premium_need = (
|
||
(ca * call_q + pa * put_q) * (1.0 + fee_rate)
|
||
)
|
||
else:
|
||
premium_need = None
|
||
margin_need = 0.0
|
||
perp_qty = 0.0
|
||
else:
|
||
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 hedge == "option_option":
|
||
perp_ok = True
|
||
elif margin_need is None or have_perp is None:
|
||
perp_ok = None
|
||
else:
|
||
perp_ok = float(have_perp) + 1e-9 >= float(margin_need)
|
||
|
||
opt_ok: bool | None
|
||
if premium_need is None or have_opt is None:
|
||
opt_ok = None
|
||
else:
|
||
opt_ok = float(have_opt) + 1e-9 >= float(premium_need)
|
||
|
||
if hedge == "option_option":
|
||
funds_ok = opt_ok is True
|
||
else:
|
||
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 hedge == "option_option":
|
||
perp_label = "永续 —(期期)"
|
||
elif 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 {
|
||
"hedge_mode": hedge,
|
||
"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 funds_gate_blocks(cap: dict[str, Any] | None) -> tuple[bool, str]:
|
||
"""
|
||
Fail-closed:永期需永续+期权均为 True;期期仅需期权为 True。
|
||
None(未知)或 False → 拦截。
|
||
"""
|
||
if not cap:
|
||
return True, "资金可开判定结果为空,拒绝开仓"
|
||
hedge = str(cap.get("hedge_mode") or "perp_option").strip().lower()
|
||
if hedge == "option_option":
|
||
if cap.get("option_can_open") is not True:
|
||
detail = (
|
||
f"{cap.get('option_label')};"
|
||
f"期权需≈{cap.get('option_need_usdc')}U/有{cap.get('option_have_usdc')}U"
|
||
)
|
||
if cap.get("option_can_open") is None:
|
||
detail += "(余额/盘口未知,fail-closed 拒绝开仓)"
|
||
return True, f"资金不足或状态未知,暂不可开新仓:{detail}"
|
||
return False, ""
|
||
if cap.get("perp_can_open") is not True or cap.get("option_can_open") is not True:
|
||
detail = (
|
||
f"{cap.get('perp_label')} · {cap.get('option_label')};"
|
||
f"永续需≈{cap.get('perp_need_usdt')}U/有{cap.get('perp_have_usdt')}U,"
|
||
f"期权需≈{cap.get('option_need_usdc')}U/有{cap.get('option_have_usdc')}U"
|
||
)
|
||
if cap.get("perp_can_open") is None or cap.get("option_can_open") is None:
|
||
detail += "(余额/盘口未知,fail-closed 拒绝开仓)"
|
||
return True, f"资金不足或状态未知,暂不可开新仓:{detail}"
|
||
return False, ""
|
||
|
||
|
||
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],
|
||
"OKX 开仓前会尝试交易账户市价兑 USDC;仍不足请检查交易账户余额。",
|
||
],
|
||
)
|
||
)
|
||
except Exception:
|
||
logger.exception("wecom funds notify failed")
|