diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index c2ad36d..dbf9a0d 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -114,11 +114,6 @@ class StrategyEngine: "funds_ok": False, "leverage": leverage, } - if open_cap.get("perp_can_open") is False or open_cap.get("option_can_open") is False: - try: - maybe_notify_funds_short(open_cap) - except Exception: - pass return { "running": bool(row["running"]), "phase": row["phase"], diff --git a/backend/app/strategy/open_capacity.py b/backend/app/strategy/open_capacity.py index 8d5f77d..990976c 100644 --- a/backend/app/strategy/open_capacity.py +++ b/backend/app/strategy/open_capacity.py @@ -1,4 +1,4 @@ -"""开仓资金可开判定:永续保证金 + 期权权利金。""" +"""开仓资金可开判定:只看交易账户(永续 USDT / 期权 USDC)。""" from __future__ import annotations @@ -16,9 +16,8 @@ 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 分钟内不重复推 +# 不足期间只推一次;资金恢复后清零,下次再不足可再推一次 +_notified_while_short: bool = False def _f(v: Any) -> float | None: @@ -57,9 +56,7 @@ def _live_balances() -> dict[str, float | None]: 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, - "funding_usdc": None, "trading_usdc": None, } try: @@ -68,8 +65,8 @@ def _live_balances() -> dict[str, float | None]: client = OkxFundsClient() try: bal = client.fetch_balances() - for k in out: - out[k] = _f(bal.get(k)) + out["trading_usdt"] = _f(bal.get("trading_usdt")) + out["trading_usdc"] = _f(bal.get("trading_usdc")) finally: client.close() except Exception as e: @@ -80,31 +77,21 @@ def _live_balances() -> dict[str, float | None]: def _sim_balances(db: Database) -> dict[str, float]: + """只看交易账户,不看资金账户。""" w = SimFundsWallets(db).view() - led = Ledger(db).snapshot() - avail = float(led.get("available") or 0) - trading_usdt = float(w.get("trading_usdt") or 0) - trading_usdc = float(w.get("trading_usdc") or 0) - # 未划转到交易账户时回退账本可用(兼容旧 SIM) - if trading_usdt < 1e-9 and trading_usdc < 1e-9 and avail > 1e-9: - return { - "perp_usdt": avail, - "option_usdc": avail, - "ledger_available": avail, - } return { - "perp_usdt": trading_usdt, - "option_usdc": trading_usdc, - "ledger_available": avail, + "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(或 SIM 回退可用) >= 卖一×名义×(1+费率) + 返回永续/期权是否有足够交易账户资金开新仓。 + - 永续:交易账户 USDT >= 名义/杠杆 + - 期权:交易账户 USDC >= 卖一×名义×(1+费率) """ + global _notified_while_short db = db or get_db() s = get_settings() ledger = Ledger(db) @@ -127,7 +114,6 @@ def assess_open_capacity(db: Database | None = None) -> dict[str, Any]: have_opt = float(bal["option_usdc"]) else: live = _live_balances() - # OKX:永续与期权都在交易账户 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 @@ -145,6 +131,11 @@ def assess_open_capacity(db: Database | None = None) -> dict[str, Any]: 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 可开" @@ -174,43 +165,43 @@ def assess_open_capacity(db: Database | None = None) -> dict[str, Any]: "option_can_open": opt_ok, "perp_label": perp_label, "option_label": opt_label, - "funds_ok": (perp_ok is True and opt_ok is True), + "funds_ok": funds_ok, + "source": "trading", } def maybe_notify_funds_short(cap: dict[str, Any] | None = None) -> None: - """资金不足时企业微信推送(去重)。""" - global _last_notify_key, _last_notify_ms + """交易账户资金不足时企业微信推送:不足期间只推一次。""" + global _notified_while_short cap = cap or assess_open_capacity() + short = cap.get("perp_can_open") is False or cap.get("option_can_open") is False + if not short: + _notified_while_short = False + return + if _notified_while_short: + return + _notified_while_short = True 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" + 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"期权不足:需约 {cap.get('option_need_usdc')}U,现有 {cap.get('option_have_usdc')}U" + f"交易账户 USDC 不足:需约 {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="资金不足 · 无法开新仓", + title="交易账户资金不足 · 无法开新仓", lines=[ f"**永续**: {cap.get('perp_label')}", f"**期权**: {cap.get('option_label')}", *[f"**详情**: {p}" for p in parts], - "请划转/兑换后重试。", + "请从资金账户划转到交易账户后重试。", ], ) ) diff --git a/docs/更新说明.md b/docs/更新说明.md index 4c6c6da..f22810e 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,16 @@ --- +## 2026-07-29 — 可开判定只看交易账户;不足只通知一次;去重标题 + +### 变更 + +1. 权益/杠杆可开判定仅看**交易账户** USDT/USDC(不再回退资金账户)。 +2. 资金不足企业微信:**不足期间只推一次**,恢复后再次不足才再推;计划页轮询不再触发推送。 +3. 去掉计划页「自动对冲计划」标题(与导航重复)。 + +--- + ## 2026-07-29 — 资金条:USDT/USDC 两行 + 居中对齐 ### 变更 diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index 9e2c0cb..b820bd5 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -166,10 +166,6 @@ export default function PlanPage() { return (