diff --git a/backend/app/api/funds.py b/backend/app/api/funds.py index a9e49f5..fac0f20 100644 --- a/backend/app/api/funds.py +++ b/backend/app/api/funds.py @@ -34,12 +34,12 @@ def _pl_ratio(pnls: list[float]) -> float | None: return round(avg_w / avg_l, 2) -def _fmt_opt(usdc: float | None, usdt: float | None) -> str: +def _fmt_acct(usdt: float | None, usdc: float | None) -> str: parts: list[str] = [] - if usdc is not None: + if usdt is not None: + parts.append(f"{usdt:.2f}U") + if usdc is not None and abs(usdc) > 1e-8: parts.append(f"{usdc:.2f} USDC") - if usdt is not None and abs(usdt) > 1e-8: - parts.append(f"{usdt:.2f} USDT") return " + ".join(parts) if parts else "—" @@ -51,8 +51,8 @@ class ConvertBody(BaseModel): class TransferBody(BaseModel): ccy: Literal["USDT", "USDC", "usdt", "usdc"] = "USDC" amount: float = Field(gt=0) - from_account: str - to_account: str + from_account: Literal["funding", "trading"] = "funding" + to_account: Literal["funding", "trading"] = "trading" @router.get("/summary") @@ -96,18 +96,16 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st eq = float(Ledger(db).snapshot().get("equity") or 0) if eq > 0: w = wallets.reset_from_equity(eq) - funding_usdt = float(w["funding_usdt"]) - trading_usdt = float(w["trading_usdt"]) - opt_f_usdc = float(w["options_funding_usdc"]) - opt_t_usdc = float(w["options_trading_usdc"]) - opt_f_usdt = float(w["options_funding_usdt"]) - opt_t_usdt = float(w["options_trading_usdt"]) - total = wallets.total_usdt_equiv(w) + v = wallets.view() + funding_usdt = float(v["funding_usdt"]) + trading_usdt = float(v["trading_usdt"]) + funding_usdc = float(v["funding_usdc"]) + trading_usdc = float(v["trading_usdc"]) + total = wallets.total_usdt_equiv() rate = usdc_usdt_mid_rate() else: rate = usdc_usdt_mid_rate() - funding_usdt = trading_usdt = None - opt_f_usdc = opt_t_usdc = opt_f_usdt = opt_t_usdt = None + funding_usdt = trading_usdt = funding_usdc = trading_usdc = None total = None if exchange == "OKX": client = OkxFundsClient() @@ -115,18 +113,9 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st bal = client.fetch_balances() funding_usdt = bal.get("funding_usdt") trading_usdt = bal.get("trading_usdt") - opt_f_usdc = bal.get("options_funding_usdc") - opt_t_usdc = bal.get("options_trading_usdc") - opt_f_usdt = bal.get("options_funding_usdt") - opt_t_usdt = bal.get("options_trading_usdt") - parts = [ - funding_usdt, - trading_usdt, - opt_f_usdc, - opt_t_usdc, - opt_f_usdt, - opt_t_usdt, - ] + funding_usdc = bal.get("funding_usdc") + trading_usdc = bal.get("trading_usdc") + parts = [funding_usdt, trading_usdt, funding_usdc, trading_usdc] vals = [float(x) for x in parts if x is not None] total = round(sum(vals), 2) if vals else None except Exception as e: @@ -155,12 +144,10 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st "total_funds": total, "funding_usdt": funding_usdt, "trading_usdt": trading_usdt, - "options_funding_usdc": opt_f_usdc, - "options_trading_usdc": opt_t_usdc, - "options_funding_usdt": opt_f_usdt, - "options_trading_usdt": opt_t_usdt, - "options_funding_label": _fmt_opt(opt_f_usdc, opt_f_usdt), - "options_trading_label": _fmt_opt(opt_t_usdc, opt_t_usdt), + "funding_usdc": funding_usdc, + "trading_usdc": trading_usdc, + "funding_label": _fmt_acct(funding_usdt, funding_usdc), + "trading_label": _fmt_acct(trading_usdt, trading_usdc), "realtime_pnl": realtime, "usdc_usdt_rate": rate, "perp_inst_id": str( diff --git a/backend/app/live/okx_funds.py b/backend/app/live/okx_funds.py index 6b8996f..8594e71 100644 --- a/backend/app/live/okx_funds.py +++ b/backend/app/live/okx_funds.py @@ -14,8 +14,6 @@ _ACCT_CODE = { "funding": "6", "trading": "18", "spot": "18", - "options_funding": "6", - "options_trading": "18", } @@ -46,10 +44,6 @@ class OkxFundsClient: "funding_usdc": None, "trading_usdt": None, "trading_usdc": None, - "options_funding_usdc": None, - "options_trading_usdc": None, - "options_funding_usdt": None, - "options_trading_usdt": None, } try: rows = self.trade._request("GET", "/api/v5/asset/balances") @@ -58,10 +52,8 @@ class OkxFundsClient: bal = _f(row.get("bal")) or _f(row.get("availBal")) if ccy == "USDT": out["funding_usdt"] = bal - out["options_funding_usdt"] = bal elif ccy == "USDC": out["funding_usdc"] = bal - out["options_funding_usdc"] = bal except Exception as e: logger.warning("OKX asset balances failed: %s", e) @@ -78,10 +70,8 @@ class OkxFundsClient: eq = _f(row.get("eq")) or _f(row.get("cashBal")) or _f(row.get("availBal")) if ccy == "USDT": out["trading_usdt"] = eq - out["options_trading_usdt"] = eq elif ccy == "USDC": out["trading_usdc"] = eq - out["options_trading_usdc"] = eq except Exception as e: logger.warning("OKX account balance failed: %s", e) @@ -148,7 +138,7 @@ class OkxFundsClient: from_code = _ACCT_CODE.get(fa) to_code = _ACCT_CODE.get(ta) if not from_code or not to_code: - return {"ok": False, "detail": "账户须为 funding/trading(或 options_* 别名)"} + return {"ok": False, "detail": "账户须为 funding / trading"} body = { "ccy": str(ccy).upper(), "amt": str(amt), diff --git a/backend/app/sim/funds_wallets.py b/backend/app/sim/funds_wallets.py index afa804a..108d1c0 100644 --- a/backend/app/sim/funds_wallets.py +++ b/backend/app/sim/funds_wallets.py @@ -1,4 +1,4 @@ -"""SIM 多账户资金:资金/交易 USDT + 期权资金/交易 USDC(对齐 OKX 展示)。""" +"""SIM 资金钱包:资金账户 / 交易账户 × USDT|USDC(对齐 OKX,无期权分账户)。""" from __future__ import annotations @@ -7,24 +7,19 @@ from typing import Any from ..models.db import Database, get_db +# DB 列名(历史兼容:USDC 存在 options_*_usdc 列,语义为资金/交易账户 USDC) WALLET_KEYS = ( "funding_usdt", "trading_usdt", - "options_funding_usdc", - "options_trading_usdc", - "options_funding_usdt", - "options_trading_usdt", + "options_funding_usdc", # = funding_usdc + "options_trading_usdc", # = trading_usdc + "options_funding_usdt", # 废弃,恒为 0 + "options_trading_usdt", # 废弃,恒为 0 ) -# 划转账户映射 _ACCT_MAP = { ("funding", "usdt"): "funding_usdt", ("trading", "usdt"): "trading_usdt", - ("options_funding", "usdc"): "options_funding_usdc", - ("options_trading", "usdc"): "options_trading_usdc", - ("options_funding", "usdt"): "options_funding_usdt", - ("options_trading", "usdt"): "options_trading_usdt", - # 简化别名:funding/trading + usdc → 期权侧 ("funding", "usdc"): "options_funding_usdc", ("trading", "usdc"): "options_trading_usdc", } @@ -44,10 +39,36 @@ class SimFundsWallets: return {k: 0.0 for k in WALLET_KEYS} return {k: float(row[k] or 0) for k in WALLET_KEYS} + def view(self) -> dict[str, float]: + """对外口径:funding/trading × usdt/usdc。""" + s = self.snapshot() + return { + "funding_usdt": float(s["funding_usdt"]), + "trading_usdt": float(s["trading_usdt"]), + "funding_usdc": float(s["options_funding_usdc"]), + "trading_usdc": float(s["options_trading_usdc"]), + } + def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float: - """USDC 按 1:1 计入总资金(与 crypto_monitor 一致)。""" - s = snap or self.snapshot() - return round(sum(float(s.get(k) or 0) for k in WALLET_KEYS), 8) + """USDC 按 1:1 计入总资金。""" + if snap is None: + v = self.view() + elif "funding_usdc" in snap: + v = snap + else: + v = { + "funding_usdt": float(snap.get("funding_usdt") or 0), + "trading_usdt": float(snap.get("trading_usdt") or 0), + "funding_usdc": float(snap.get("options_funding_usdc") or 0), + "trading_usdc": float(snap.get("options_trading_usdc") or 0), + } + return round( + float(v.get("funding_usdt") or 0) + + float(v.get("trading_usdt") or 0) + + float(v.get("funding_usdc") or 0) + + float(v.get("trading_usdc") or 0), + 8, + ) def reset_from_equity(self, equity: float) -> dict[str, float]: """重置:全部放入资金账户 USDT。""" @@ -71,22 +92,20 @@ class SimFundsWallets: self.db.execute( """UPDATE funds_wallets SET funding_usdt=?, trading_usdt=?, options_funding_usdc=?, options_trading_usdc=?, - options_funding_usdt=?, options_trading_usdt=?, updated_at_ms=? + options_funding_usdt=0, options_trading_usdt=0, updated_at_ms=? WHERE id=1""", ( snap["funding_usdt"], snap["trading_usdt"], snap["options_funding_usdc"], snap["options_trading_usdc"], - snap["options_funding_usdt"], - snap["options_trading_usdt"], now, ), ) return snap def mirror_cash(self, amount: float, *, kind: str) -> None: - """策略账本变动时镜像到对应钱包(SIM)。""" + """策略账本变动镜像到交易账户(永续 USDT / 期权 USDC)。""" amt = float(amount) if abs(amt) < 1e-12: return @@ -94,16 +113,12 @@ class SimFundsWallets: k = (kind or "").lower() if "option" in k: key = "options_trading_usdc" - elif "perp" in k or "funding" in k: - key = "trading_usdt" else: key = "trading_usdt" snap[key] = float(snap.get(key) or 0) + amt - # 允许短暂为负(与 ledger allow_negative 对齐时由调用方保证);展示侧夹到合理范围不在此做 self._set(**snap) def sync_ledger_equity(self) -> float: - """兑换/划转后把总权益同步到 ledger_meta(不重置钱包分配)。""" total = self.total_usdt_equiv() now = _now_ms() self.db.execute( @@ -119,11 +134,7 @@ class SimFundsWallets: amount: float, rate: float = 1.0, ) -> dict[str, Any]: - """ - SIM 兑换(默认在「资金账户」内 USDT↔USDC,对齐 crypto_monitor 主路径)。 - direction: usdt_to_usdc | usdc_to_usdt - rate: 1 USDC = rate USDT(默认 1.0) - """ + """资金账户内 USDT↔USDC 兑换。""" amt = float(amount) if amt <= 0: return {"ok": False, "detail": "数量须大于 0"} @@ -140,7 +151,7 @@ class SimFundsWallets: elif d == "usdc_to_usdt": src = float(snap["options_funding_usdc"]) if amt > src + 1e-9: - return {"ok": False, "detail": f"期权资金账户 USDC 不足(可用 {src:.4f})"} + return {"ok": False, "detail": f"资金账户 USDC 不足(可用 {src:.4f})"} usdt = amt * r snap["options_funding_usdc"] = src - amt snap["funding_usdt"] = float(snap["funding_usdt"]) + usdt @@ -154,7 +165,7 @@ class SimFundsWallets: "direction": d, "amount": amt, "rate": r, - "wallets": self.snapshot(), + "wallets": self.view(), "total_usdt_equiv": total, } @@ -166,26 +177,26 @@ class SimFundsWallets: from_account: str, to_account: str, ) -> dict[str, Any]: - """SIM 划转:funding/trading/options_funding/options_trading × USDT|USDC。""" + """仅资金账户 ↔ 交易账户。""" amt = float(amount) if amt <= 0: return {"ok": False, "detail": "划转金额须大于 0"} ccy_l = (ccy or "USDC").strip().lower() fa = (from_account or "").strip().lower() ta = (to_account or "").strip().lower() + allowed = {"funding", "trading"} + if fa not in allowed or ta not in allowed: + return {"ok": False, "detail": "账户仅支持 funding / trading"} if fa == ta: return {"ok": False, "detail": "来源与目标账户不能相同"} src_key = _ACCT_MAP.get((fa, ccy_l)) dst_key = _ACCT_MAP.get((ta, ccy_l)) if not src_key or not dst_key: - return { - "ok": False, - "detail": "账户须为 funding/trading/options_funding/options_trading,币种 USDT|USDC", - } + return {"ok": False, "detail": "币种须为 USDT 或 USDC"} snap = self.snapshot() src_bal = float(snap[src_key]) if amt > src_bal + 1e-9: - return {"ok": False, "detail": f"{src_key} 余额不足(可用 {src_bal:.4f})"} + return {"ok": False, "detail": f"余额不足(可用 {src_bal:.4f})"} snap[src_key] = src_bal - amt snap[dst_key] = float(snap[dst_key]) + amt self._set(**snap) @@ -197,6 +208,6 @@ class SimFundsWallets: "amount": amt, "from": fa, "to": ta, - "wallets": self.snapshot(), + "wallets": self.view(), "total_usdt_equiv": total, } diff --git a/backend/app/strategy/open_capacity.py b/backend/app/strategy/open_capacity.py index 5335efc..8d5f77d 100644 --- a/backend/app/strategy/open_capacity.py +++ b/backend/app/strategy/open_capacity.py @@ -59,8 +59,8 @@ def _live_balances() -> dict[str, float | None]: out: dict[str, float | None] = { "funding_usdt": None, "trading_usdt": None, - "options_funding_usdc": None, - "options_trading_usdc": None, + "funding_usdc": None, + "trading_usdc": None, } try: from ..live.okx_funds import OkxFundsClient @@ -80,24 +80,21 @@ def _live_balances() -> dict[str, float | None]: def _sim_balances(db: Database) -> dict[str, float]: - w = SimFundsWallets(db).snapshot() + w = SimFundsWallets(db).view() 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 + 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": usdt, - "option_usdc": usdc, + "perp_usdt": trading_usdt, + "option_usdc": trading_usdc, "ledger_available": avail, } @@ -130,21 +127,11 @@ def assess_open_capacity(db: Database | None = None) -> dict[str, Any]: 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) + # 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 + 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: diff --git a/docs/更新说明.md b/docs/更新说明.md index e22458e..ecf2f29 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,16 @@ --- +## 2026-07-29 — 资金口径对齐 OKX:仅资金↔交易 + +### 变更 + +1. 去掉「期权资金 / 期权交易」账户;顶栏只显示资金账户、交易账户(各含 USDT+USDC)。 +2. 划转仅支持资金账户 ↔ 交易账户;兑换在资金账户内 USDT↔USDC。 +3. 资金页说明收入折叠「规则说明」;划转控件单行排列;设置卡片与资金条同宽。 + +--- + ## 2026-07-29 — 权益杠杆可开判定 + 资金不足微信推送 ### 变更 diff --git a/frontend/src/components/FundsBar.tsx b/frontend/src/components/FundsBar.tsx index 2e3f12b..56d6c5b 100644 --- a/frontend/src/components/FundsBar.tsx +++ b/frontend/src/components/FundsBar.tsx @@ -11,8 +11,10 @@ export type FundsSummary = { total_funds: number | null; funding_usdt: number | null; trading_usdt: number | null; - options_funding_label: string; - options_trading_label: string; + funding_usdc?: number | null; + trading_usdc?: number | null; + funding_label?: string; + trading_label?: string; realtime_pnl: number | null; usdc_usdt_rate?: number; perp_inst_id?: string; @@ -84,19 +86,15 @@ export default function FundsBar() {
+ 币种兑换:对齐 OKX 现货市价 USDC-USDT,在资金账户 + 内完成 USDT↔USDC。参考价{" "} + {usdcRate != null ? `1 USDC ≈ ${usdcRate.toFixed(4)} USDT` : "—"}。 +
++ 账户划转:仅资金账户 ↔ 交易账户 + (USDT/USDC)。OKX 永续与期权均在交易账户,无单独期权资金/期权交易账户。 +
+- 对齐 OKX 现货市价 USDC-USDT。参考价{" "} - {usdcRate != null ? `1 USDC ≈ ${usdcRate.toFixed(4)} USDT` : "—"} - 。兑换发生在资金账户侧(USDT↔期权资金 USDC)。 -
- 资金账户 ↔ 交易账户(USDT/USDC)。期权侧可用 options_funding / - options_trading。 -
-