Align funds to OKX funding/trading only; fold rules; match settings width.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+20
-33
@@ -34,12 +34,12 @@ def _pl_ratio(pnls: list[float]) -> float | None:
|
|||||||
return round(avg_w / avg_l, 2)
|
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] = []
|
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")
|
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 "—"
|
return " + ".join(parts) if parts else "—"
|
||||||
|
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ class ConvertBody(BaseModel):
|
|||||||
class TransferBody(BaseModel):
|
class TransferBody(BaseModel):
|
||||||
ccy: Literal["USDT", "USDC", "usdt", "usdc"] = "USDC"
|
ccy: Literal["USDT", "USDC", "usdt", "usdc"] = "USDC"
|
||||||
amount: float = Field(gt=0)
|
amount: float = Field(gt=0)
|
||||||
from_account: str
|
from_account: Literal["funding", "trading"] = "funding"
|
||||||
to_account: str
|
to_account: Literal["funding", "trading"] = "trading"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/summary")
|
@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)
|
eq = float(Ledger(db).snapshot().get("equity") or 0)
|
||||||
if eq > 0:
|
if eq > 0:
|
||||||
w = wallets.reset_from_equity(eq)
|
w = wallets.reset_from_equity(eq)
|
||||||
funding_usdt = float(w["funding_usdt"])
|
v = wallets.view()
|
||||||
trading_usdt = float(w["trading_usdt"])
|
funding_usdt = float(v["funding_usdt"])
|
||||||
opt_f_usdc = float(w["options_funding_usdc"])
|
trading_usdt = float(v["trading_usdt"])
|
||||||
opt_t_usdc = float(w["options_trading_usdc"])
|
funding_usdc = float(v["funding_usdc"])
|
||||||
opt_f_usdt = float(w["options_funding_usdt"])
|
trading_usdc = float(v["trading_usdc"])
|
||||||
opt_t_usdt = float(w["options_trading_usdt"])
|
total = wallets.total_usdt_equiv()
|
||||||
total = wallets.total_usdt_equiv(w)
|
|
||||||
rate = usdc_usdt_mid_rate()
|
rate = usdc_usdt_mid_rate()
|
||||||
else:
|
else:
|
||||||
rate = usdc_usdt_mid_rate()
|
rate = usdc_usdt_mid_rate()
|
||||||
funding_usdt = trading_usdt = None
|
funding_usdt = trading_usdt = funding_usdc = trading_usdc = None
|
||||||
opt_f_usdc = opt_t_usdc = opt_f_usdt = opt_t_usdt = None
|
|
||||||
total = None
|
total = None
|
||||||
if exchange == "OKX":
|
if exchange == "OKX":
|
||||||
client = OkxFundsClient()
|
client = OkxFundsClient()
|
||||||
@@ -115,18 +113,9 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st
|
|||||||
bal = client.fetch_balances()
|
bal = client.fetch_balances()
|
||||||
funding_usdt = bal.get("funding_usdt")
|
funding_usdt = bal.get("funding_usdt")
|
||||||
trading_usdt = bal.get("trading_usdt")
|
trading_usdt = bal.get("trading_usdt")
|
||||||
opt_f_usdc = bal.get("options_funding_usdc")
|
funding_usdc = bal.get("funding_usdc")
|
||||||
opt_t_usdc = bal.get("options_trading_usdc")
|
trading_usdc = bal.get("trading_usdc")
|
||||||
opt_f_usdt = bal.get("options_funding_usdt")
|
parts = [funding_usdt, trading_usdt, funding_usdc, trading_usdc]
|
||||||
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,
|
|
||||||
]
|
|
||||||
vals = [float(x) for x in parts if x is not None]
|
vals = [float(x) for x in parts if x is not None]
|
||||||
total = round(sum(vals), 2) if vals else None
|
total = round(sum(vals), 2) if vals else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -155,12 +144,10 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st
|
|||||||
"total_funds": total,
|
"total_funds": total,
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
"trading_usdt": trading_usdt,
|
"trading_usdt": trading_usdt,
|
||||||
"options_funding_usdc": opt_f_usdc,
|
"funding_usdc": funding_usdc,
|
||||||
"options_trading_usdc": opt_t_usdc,
|
"trading_usdc": trading_usdc,
|
||||||
"options_funding_usdt": opt_f_usdt,
|
"funding_label": _fmt_acct(funding_usdt, funding_usdc),
|
||||||
"options_trading_usdt": opt_t_usdt,
|
"trading_label": _fmt_acct(trading_usdt, trading_usdc),
|
||||||
"options_funding_label": _fmt_opt(opt_f_usdc, opt_f_usdt),
|
|
||||||
"options_trading_label": _fmt_opt(opt_t_usdc, opt_t_usdt),
|
|
||||||
"realtime_pnl": realtime,
|
"realtime_pnl": realtime,
|
||||||
"usdc_usdt_rate": rate,
|
"usdc_usdt_rate": rate,
|
||||||
"perp_inst_id": str(
|
"perp_inst_id": str(
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ _ACCT_CODE = {
|
|||||||
"funding": "6",
|
"funding": "6",
|
||||||
"trading": "18",
|
"trading": "18",
|
||||||
"spot": "18",
|
"spot": "18",
|
||||||
"options_funding": "6",
|
|
||||||
"options_trading": "18",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -46,10 +44,6 @@ class OkxFundsClient:
|
|||||||
"funding_usdc": None,
|
"funding_usdc": None,
|
||||||
"trading_usdt": None,
|
"trading_usdt": None,
|
||||||
"trading_usdc": None,
|
"trading_usdc": None,
|
||||||
"options_funding_usdc": None,
|
|
||||||
"options_trading_usdc": None,
|
|
||||||
"options_funding_usdt": None,
|
|
||||||
"options_trading_usdt": None,
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
rows = self.trade._request("GET", "/api/v5/asset/balances")
|
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"))
|
bal = _f(row.get("bal")) or _f(row.get("availBal"))
|
||||||
if ccy == "USDT":
|
if ccy == "USDT":
|
||||||
out["funding_usdt"] = bal
|
out["funding_usdt"] = bal
|
||||||
out["options_funding_usdt"] = bal
|
|
||||||
elif ccy == "USDC":
|
elif ccy == "USDC":
|
||||||
out["funding_usdc"] = bal
|
out["funding_usdc"] = bal
|
||||||
out["options_funding_usdc"] = bal
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("OKX asset balances failed: %s", 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"))
|
eq = _f(row.get("eq")) or _f(row.get("cashBal")) or _f(row.get("availBal"))
|
||||||
if ccy == "USDT":
|
if ccy == "USDT":
|
||||||
out["trading_usdt"] = eq
|
out["trading_usdt"] = eq
|
||||||
out["options_trading_usdt"] = eq
|
|
||||||
elif ccy == "USDC":
|
elif ccy == "USDC":
|
||||||
out["trading_usdc"] = eq
|
out["trading_usdc"] = eq
|
||||||
out["options_trading_usdc"] = eq
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("OKX account balance failed: %s", e)
|
logger.warning("OKX account balance failed: %s", e)
|
||||||
|
|
||||||
@@ -148,7 +138,7 @@ class OkxFundsClient:
|
|||||||
from_code = _ACCT_CODE.get(fa)
|
from_code = _ACCT_CODE.get(fa)
|
||||||
to_code = _ACCT_CODE.get(ta)
|
to_code = _ACCT_CODE.get(ta)
|
||||||
if not from_code or not to_code:
|
if not from_code or not to_code:
|
||||||
return {"ok": False, "detail": "账户须为 funding/trading(或 options_* 别名)"}
|
return {"ok": False, "detail": "账户须为 funding / trading"}
|
||||||
body = {
|
body = {
|
||||||
"ccy": str(ccy).upper(),
|
"ccy": str(ccy).upper(),
|
||||||
"amt": str(amt),
|
"amt": str(amt),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""SIM 多账户资金:资金/交易 USDT + 期权资金/交易 USDC(对齐 OKX 展示)。"""
|
"""SIM 资金钱包:资金账户 / 交易账户 × USDT|USDC(对齐 OKX,无期权分账户)。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -7,24 +7,19 @@ from typing import Any
|
|||||||
|
|
||||||
from ..models.db import Database, get_db
|
from ..models.db import Database, get_db
|
||||||
|
|
||||||
|
# DB 列名(历史兼容:USDC 存在 options_*_usdc 列,语义为资金/交易账户 USDC)
|
||||||
WALLET_KEYS = (
|
WALLET_KEYS = (
|
||||||
"funding_usdt",
|
"funding_usdt",
|
||||||
"trading_usdt",
|
"trading_usdt",
|
||||||
"options_funding_usdc",
|
"options_funding_usdc", # = funding_usdc
|
||||||
"options_trading_usdc",
|
"options_trading_usdc", # = trading_usdc
|
||||||
"options_funding_usdt",
|
"options_funding_usdt", # 废弃,恒为 0
|
||||||
"options_trading_usdt",
|
"options_trading_usdt", # 废弃,恒为 0
|
||||||
)
|
)
|
||||||
|
|
||||||
# 划转账户映射
|
|
||||||
_ACCT_MAP = {
|
_ACCT_MAP = {
|
||||||
("funding", "usdt"): "funding_usdt",
|
("funding", "usdt"): "funding_usdt",
|
||||||
("trading", "usdt"): "trading_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",
|
("funding", "usdc"): "options_funding_usdc",
|
||||||
("trading", "usdc"): "options_trading_usdc",
|
("trading", "usdc"): "options_trading_usdc",
|
||||||
}
|
}
|
||||||
@@ -44,10 +39,36 @@ class SimFundsWallets:
|
|||||||
return {k: 0.0 for k in WALLET_KEYS}
|
return {k: 0.0 for k in WALLET_KEYS}
|
||||||
return {k: float(row[k] or 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:
|
def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float:
|
||||||
"""USDC 按 1:1 计入总资金(与 crypto_monitor 一致)。"""
|
"""USDC 按 1:1 计入总资金。"""
|
||||||
s = snap or self.snapshot()
|
if snap is None:
|
||||||
return round(sum(float(s.get(k) or 0) for k in WALLET_KEYS), 8)
|
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]:
|
def reset_from_equity(self, equity: float) -> dict[str, float]:
|
||||||
"""重置:全部放入资金账户 USDT。"""
|
"""重置:全部放入资金账户 USDT。"""
|
||||||
@@ -71,22 +92,20 @@ class SimFundsWallets:
|
|||||||
self.db.execute(
|
self.db.execute(
|
||||||
"""UPDATE funds_wallets SET
|
"""UPDATE funds_wallets SET
|
||||||
funding_usdt=?, trading_usdt=?, options_funding_usdc=?, options_trading_usdc=?,
|
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""",
|
WHERE id=1""",
|
||||||
(
|
(
|
||||||
snap["funding_usdt"],
|
snap["funding_usdt"],
|
||||||
snap["trading_usdt"],
|
snap["trading_usdt"],
|
||||||
snap["options_funding_usdc"],
|
snap["options_funding_usdc"],
|
||||||
snap["options_trading_usdc"],
|
snap["options_trading_usdc"],
|
||||||
snap["options_funding_usdt"],
|
|
||||||
snap["options_trading_usdt"],
|
|
||||||
now,
|
now,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return snap
|
return snap
|
||||||
|
|
||||||
def mirror_cash(self, amount: float, *, kind: str) -> None:
|
def mirror_cash(self, amount: float, *, kind: str) -> None:
|
||||||
"""策略账本变动时镜像到对应钱包(SIM)。"""
|
"""策略账本变动镜像到交易账户(永续 USDT / 期权 USDC)。"""
|
||||||
amt = float(amount)
|
amt = float(amount)
|
||||||
if abs(amt) < 1e-12:
|
if abs(amt) < 1e-12:
|
||||||
return
|
return
|
||||||
@@ -94,16 +113,12 @@ class SimFundsWallets:
|
|||||||
k = (kind or "").lower()
|
k = (kind or "").lower()
|
||||||
if "option" in k:
|
if "option" in k:
|
||||||
key = "options_trading_usdc"
|
key = "options_trading_usdc"
|
||||||
elif "perp" in k or "funding" in k:
|
|
||||||
key = "trading_usdt"
|
|
||||||
else:
|
else:
|
||||||
key = "trading_usdt"
|
key = "trading_usdt"
|
||||||
snap[key] = float(snap.get(key) or 0) + amt
|
snap[key] = float(snap.get(key) or 0) + amt
|
||||||
# 允许短暂为负(与 ledger allow_negative 对齐时由调用方保证);展示侧夹到合理范围不在此做
|
|
||||||
self._set(**snap)
|
self._set(**snap)
|
||||||
|
|
||||||
def sync_ledger_equity(self) -> float:
|
def sync_ledger_equity(self) -> float:
|
||||||
"""兑换/划转后把总权益同步到 ledger_meta(不重置钱包分配)。"""
|
|
||||||
total = self.total_usdt_equiv()
|
total = self.total_usdt_equiv()
|
||||||
now = _now_ms()
|
now = _now_ms()
|
||||||
self.db.execute(
|
self.db.execute(
|
||||||
@@ -119,11 +134,7 @@ class SimFundsWallets:
|
|||||||
amount: float,
|
amount: float,
|
||||||
rate: float = 1.0,
|
rate: float = 1.0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""资金账户内 USDT↔USDC 兑换。"""
|
||||||
SIM 兑换(默认在「资金账户」内 USDT↔USDC,对齐 crypto_monitor 主路径)。
|
|
||||||
direction: usdt_to_usdc | usdc_to_usdt
|
|
||||||
rate: 1 USDC = rate USDT(默认 1.0)
|
|
||||||
"""
|
|
||||||
amt = float(amount)
|
amt = float(amount)
|
||||||
if amt <= 0:
|
if amt <= 0:
|
||||||
return {"ok": False, "detail": "数量须大于 0"}
|
return {"ok": False, "detail": "数量须大于 0"}
|
||||||
@@ -140,7 +151,7 @@ class SimFundsWallets:
|
|||||||
elif d == "usdc_to_usdt":
|
elif d == "usdc_to_usdt":
|
||||||
src = float(snap["options_funding_usdc"])
|
src = float(snap["options_funding_usdc"])
|
||||||
if amt > src + 1e-9:
|
if amt > src + 1e-9:
|
||||||
return {"ok": False, "detail": f"期权资金账户 USDC 不足(可用 {src:.4f})"}
|
return {"ok": False, "detail": f"资金账户 USDC 不足(可用 {src:.4f})"}
|
||||||
usdt = amt * r
|
usdt = amt * r
|
||||||
snap["options_funding_usdc"] = src - amt
|
snap["options_funding_usdc"] = src - amt
|
||||||
snap["funding_usdt"] = float(snap["funding_usdt"]) + usdt
|
snap["funding_usdt"] = float(snap["funding_usdt"]) + usdt
|
||||||
@@ -154,7 +165,7 @@ class SimFundsWallets:
|
|||||||
"direction": d,
|
"direction": d,
|
||||||
"amount": amt,
|
"amount": amt,
|
||||||
"rate": r,
|
"rate": r,
|
||||||
"wallets": self.snapshot(),
|
"wallets": self.view(),
|
||||||
"total_usdt_equiv": total,
|
"total_usdt_equiv": total,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,26 +177,26 @@ class SimFundsWallets:
|
|||||||
from_account: str,
|
from_account: str,
|
||||||
to_account: str,
|
to_account: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""SIM 划转:funding/trading/options_funding/options_trading × USDT|USDC。"""
|
"""仅资金账户 ↔ 交易账户。"""
|
||||||
amt = float(amount)
|
amt = float(amount)
|
||||||
if amt <= 0:
|
if amt <= 0:
|
||||||
return {"ok": False, "detail": "划转金额须大于 0"}
|
return {"ok": False, "detail": "划转金额须大于 0"}
|
||||||
ccy_l = (ccy or "USDC").strip().lower()
|
ccy_l = (ccy or "USDC").strip().lower()
|
||||||
fa = (from_account or "").strip().lower()
|
fa = (from_account or "").strip().lower()
|
||||||
ta = (to_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:
|
if fa == ta:
|
||||||
return {"ok": False, "detail": "来源与目标账户不能相同"}
|
return {"ok": False, "detail": "来源与目标账户不能相同"}
|
||||||
src_key = _ACCT_MAP.get((fa, ccy_l))
|
src_key = _ACCT_MAP.get((fa, ccy_l))
|
||||||
dst_key = _ACCT_MAP.get((ta, ccy_l))
|
dst_key = _ACCT_MAP.get((ta, ccy_l))
|
||||||
if not src_key or not dst_key:
|
if not src_key or not dst_key:
|
||||||
return {
|
return {"ok": False, "detail": "币种须为 USDT 或 USDC"}
|
||||||
"ok": False,
|
|
||||||
"detail": "账户须为 funding/trading/options_funding/options_trading,币种 USDT|USDC",
|
|
||||||
}
|
|
||||||
snap = self.snapshot()
|
snap = self.snapshot()
|
||||||
src_bal = float(snap[src_key])
|
src_bal = float(snap[src_key])
|
||||||
if amt > src_bal + 1e-9:
|
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[src_key] = src_bal - amt
|
||||||
snap[dst_key] = float(snap[dst_key]) + amt
|
snap[dst_key] = float(snap[dst_key]) + amt
|
||||||
self._set(**snap)
|
self._set(**snap)
|
||||||
@@ -197,6 +208,6 @@ class SimFundsWallets:
|
|||||||
"amount": amt,
|
"amount": amt,
|
||||||
"from": fa,
|
"from": fa,
|
||||||
"to": ta,
|
"to": ta,
|
||||||
"wallets": self.snapshot(),
|
"wallets": self.view(),
|
||||||
"total_usdt_equiv": total,
|
"total_usdt_equiv": total,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ def _live_balances() -> dict[str, float | None]:
|
|||||||
out: dict[str, float | None] = {
|
out: dict[str, float | None] = {
|
||||||
"funding_usdt": None,
|
"funding_usdt": None,
|
||||||
"trading_usdt": None,
|
"trading_usdt": None,
|
||||||
"options_funding_usdc": None,
|
"funding_usdc": None,
|
||||||
"options_trading_usdc": None,
|
"trading_usdc": None,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
from ..live.okx_funds import OkxFundsClient
|
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]:
|
def _sim_balances(db: Database) -> dict[str, float]:
|
||||||
w = SimFundsWallets(db).snapshot()
|
w = SimFundsWallets(db).view()
|
||||||
led = Ledger(db).snapshot()
|
led = Ledger(db).snapshot()
|
||||||
avail = float(led.get("available") or 0)
|
avail = float(led.get("available") or 0)
|
||||||
funding = float(w.get("funding_usdt") or 0)
|
trading_usdt = float(w.get("trading_usdt") or 0)
|
||||||
trading = float(w.get("trading_usdt") or 0)
|
trading_usdc = float(w.get("trading_usdc") or 0)
|
||||||
opt_f = float(w.get("options_funding_usdc") or 0)
|
# 未划转到交易账户时回退账本可用(兼容旧 SIM)
|
||||||
opt_t = float(w.get("options_trading_usdc") or 0)
|
if trading_usdt < 1e-9 and trading_usdc < 1e-9 and avail > 1e-9:
|
||||||
# 钱包未播种时回退账本可用
|
|
||||||
usdt = funding + trading
|
|
||||||
if usdt < 1e-9:
|
|
||||||
usdt = avail
|
|
||||||
usdc = opt_f + opt_t
|
|
||||||
if usdc < 1e-9:
|
|
||||||
# SIM 早期权利金从账本扣;无 USDC 钱包时用可用资金估期权可开
|
|
||||||
usdc = avail
|
|
||||||
return {
|
return {
|
||||||
"perp_usdt": usdt,
|
"perp_usdt": avail,
|
||||||
"option_usdc": usdc,
|
"option_usdc": avail,
|
||||||
|
"ledger_available": avail,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"perp_usdt": trading_usdt,
|
||||||
|
"option_usdc": trading_usdc,
|
||||||
"ledger_available": avail,
|
"ledger_available": avail,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,21 +127,11 @@ def assess_open_capacity(db: Database | None = None) -> dict[str, Any]:
|
|||||||
have_opt = float(bal["option_usdc"])
|
have_opt = float(bal["option_usdc"])
|
||||||
else:
|
else:
|
||||||
live = _live_balances()
|
live = _live_balances()
|
||||||
# 永续用交易账户;若为空则合并资金账户(提示需划转但仍可显示)
|
# OKX:永续与期权都在交易账户
|
||||||
t = live.get("trading_usdt")
|
t_usdt = live.get("trading_usdt")
|
||||||
f = live.get("funding_usdt")
|
t_usdc = live.get("trading_usdc")
|
||||||
if t is not None and t > 1e-9:
|
have_perp = float(t_usdt) if t_usdt is not None else None
|
||||||
have_perp = float(t)
|
have_opt = float(t_usdc) if t_usdc is not None else None
|
||||||
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
|
perp_ok: bool | None
|
||||||
if margin_need is None or have_perp is None:
|
if margin_need is None or have_perp is None:
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-29 — 资金口径对齐 OKX:仅资金↔交易
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
1. 去掉「期权资金 / 期权交易」账户;顶栏只显示资金账户、交易账户(各含 USDT+USDC)。
|
||||||
|
2. 划转仅支持资金账户 ↔ 交易账户;兑换在资金账户内 USDT↔USDC。
|
||||||
|
3. 资金页说明收入折叠「规则说明」;划转控件单行排列;设置卡片与资金条同宽。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-29 — 权益杠杆可开判定 + 资金不足微信推送
|
## 2026-07-29 — 权益杠杆可开判定 + 资金不足微信推送
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ export type FundsSummary = {
|
|||||||
total_funds: number | null;
|
total_funds: number | null;
|
||||||
funding_usdt: number | null;
|
funding_usdt: number | null;
|
||||||
trading_usdt: number | null;
|
trading_usdt: number | null;
|
||||||
options_funding_label: string;
|
funding_usdc?: number | null;
|
||||||
options_trading_label: string;
|
trading_usdc?: number | null;
|
||||||
|
funding_label?: string;
|
||||||
|
trading_label?: string;
|
||||||
realtime_pnl: number | null;
|
realtime_pnl: number | null;
|
||||||
usdc_usdt_rate?: number;
|
usdc_usdt_rate?: number;
|
||||||
perp_inst_id?: string;
|
perp_inst_id?: string;
|
||||||
@@ -84,19 +86,15 @@ export default function FundsBar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="funds-item funds-item--money">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">资金账户</div>
|
<div className="funds-label">资金账户</div>
|
||||||
<div className="funds-value mono">{fmtU(s.funding_usdt)}</div>
|
<div className="funds-value mono">
|
||||||
|
{s.funding_label || fmtU(s.funding_usdt)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item funds-item--money">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">交易账户</div>
|
<div className="funds-label">交易账户</div>
|
||||||
<div className="funds-value mono">{fmtU(s.trading_usdt)}</div>
|
<div className="funds-value mono">
|
||||||
|
{s.trading_label || fmtU(s.trading_usdt)}
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item funds-item--money">
|
|
||||||
<div className="funds-label">期权资金账户</div>
|
|
||||||
<div className="funds-value mono">{s.options_funding_label}</div>
|
|
||||||
</div>
|
|
||||||
<div className="funds-item funds-item--money">
|
|
||||||
<div className="funds-label">期权交易账户</div>
|
|
||||||
<div className="funds-value mono">{s.options_trading_label}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item funds-item--pnl funds-item--money">
|
<div className="funds-item funds-item--pnl funds-item--money">
|
||||||
<div className="funds-label">实时盈亏</div>
|
<div className="funds-label">实时盈亏</div>
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export default function SettingsPage() {
|
|||||||
const [xferFrom, setXferFrom] = useState("funding");
|
const [xferFrom, setXferFrom] = useState("funding");
|
||||||
const [xferTo, setXferTo] = useState("trading");
|
const [xferTo, setXferTo] = useState("trading");
|
||||||
const [usdcRate, setUsdcRate] = useState<number | null>(null);
|
const [usdcRate, setUsdcRate] = useState<number | null>(null);
|
||||||
|
const [fundsRulesOpen, setFundsRulesOpen] = useState(false);
|
||||||
|
|
||||||
function loadRuntime() {
|
function loadRuntime() {
|
||||||
apiFetch<RuntimeSettings>("/api/settings/runtime")
|
apiFetch<RuntimeSettings>("/api/settings/runtime")
|
||||||
@@ -1061,13 +1062,24 @@ export default function SettingsPage() {
|
|||||||
<div className="card settings-card">
|
<div className="card settings-card">
|
||||||
{fundsErr ? <div className="err">{fundsErr}</div> : null}
|
{fundsErr ? <div className="err">{fundsErr}</div> : null}
|
||||||
{fundsOk ? <div className="settings-ok">{fundsOk}</div> : null}
|
{fundsOk ? <div className="settings-ok">{fundsOk}</div> : null}
|
||||||
|
|
||||||
|
<RulesFold
|
||||||
|
open={fundsRulesOpen}
|
||||||
|
onToggle={() => setFundsRulesOpen((v) => !v)}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
币种兑换:对齐 OKX 现货市价 USDC-USDT,在<strong>资金账户</strong>
|
||||||
|
内完成 USDT↔USDC。参考价{" "}
|
||||||
|
{usdcRate != null ? `1 USDC ≈ ${usdcRate.toFixed(4)} USDT` : "—"}。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
账户划转:仅<strong>资金账户 ↔ 交易账户</strong>
|
||||||
|
(USDT/USDC)。OKX 永续与期权均在交易账户,无单独期权资金/期权交易账户。
|
||||||
|
</p>
|
||||||
|
</RulesFold>
|
||||||
|
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h3>币种兑换(USDT ↔ USDC)</h3>
|
<h3>币种兑换(USDT ↔ USDC)</h3>
|
||||||
<p className="meta" style={{ marginTop: 0 }}>
|
|
||||||
对齐 OKX 现货市价 USDC-USDT。参考价{" "}
|
|
||||||
{usdcRate != null ? `1 USDC ≈ ${usdcRate.toFixed(4)} USDT` : "—"}
|
|
||||||
。兑换发生在资金账户侧(USDT↔期权资金 USDC)。
|
|
||||||
</p>
|
|
||||||
<div className="settings-fields">
|
<div className="settings-fields">
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="convertDir">方向</label>
|
<label htmlFor="convertDir">方向</label>
|
||||||
@@ -1130,11 +1142,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h3>账户划转</h3>
|
<h3>账户划转</h3>
|
||||||
<p className="meta" style={{ marginTop: 0 }}>
|
<div className="settings-xfer-row">
|
||||||
资金账户 ↔ 交易账户(USDT/USDC)。期权侧可用 options_funding /
|
|
||||||
options_trading。
|
|
||||||
</p>
|
|
||||||
<div className="settings-fields">
|
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="xferCcy">币种</label>
|
<label htmlFor="xferCcy">币种</label>
|
||||||
<select
|
<select
|
||||||
@@ -1153,12 +1161,14 @@ export default function SettingsPage() {
|
|||||||
<select
|
<select
|
||||||
id="xferFrom"
|
id="xferFrom"
|
||||||
value={xferFrom}
|
value={xferFrom}
|
||||||
onChange={(e) => setXferFrom(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const v = e.target.value === "trading" ? "trading" : "funding";
|
||||||
|
setXferFrom(v);
|
||||||
|
setXferTo(v === "funding" ? "trading" : "funding");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="funding">资金账户</option>
|
<option value="funding">资金账户</option>
|
||||||
<option value="trading">交易账户</option>
|
<option value="trading">交易账户</option>
|
||||||
<option value="options_funding">期权资金</option>
|
|
||||||
<option value="options_trading">期权交易</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
@@ -1166,12 +1176,14 @@ export default function SettingsPage() {
|
|||||||
<select
|
<select
|
||||||
id="xferTo"
|
id="xferTo"
|
||||||
value={xferTo}
|
value={xferTo}
|
||||||
onChange={(e) => setXferTo(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const v = e.target.value === "trading" ? "trading" : "funding";
|
||||||
|
setXferTo(v);
|
||||||
|
setXferFrom(v === "funding" ? "trading" : "funding");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="funding">资金账户</option>
|
<option value="funding">资金账户</option>
|
||||||
<option value="trading">交易账户</option>
|
<option value="trading">交易账户</option>
|
||||||
<option value="options_funding">期权资金</option>
|
|
||||||
<option value="options_trading">期权交易</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
@@ -1185,8 +1197,7 @@ export default function SettingsPage() {
|
|||||||
onChange={(e) => setXferAmt(Number(e.target.value))}
|
onChange={(e) => setXferAmt(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="settings-xfer-action">
|
||||||
<div className="settings-actions">
|
|
||||||
<button
|
<button
|
||||||
className="btn"
|
className="btn"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1216,12 +1227,13 @@ export default function SettingsPage() {
|
|||||||
划转
|
划转
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{tab === "account" ? (
|
{tab === "account" ? (
|
||||||
<div className="card settings-card settings-card-narrow">
|
<div className="card settings-card">
|
||||||
{err ? <div className="err">{err}</div> : null}
|
{err ? <div className="err">{err}</div> : null}
|
||||||
{ok ? <div className="settings-ok">{ok}</div> : null}
|
{ok ? <div className="settings-ok">{ok}</div> : null}
|
||||||
<form onSubmit={onSaveCreds} className="settings-account-form">
|
<form onSubmit={onSaveCreds} className="settings-account-form">
|
||||||
|
|||||||
@@ -1118,18 +1118,19 @@ input {
|
|||||||
border-color: rgba(240, 185, 11, 0.55);
|
border-color: rgba(240, 185, 11, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings: ultrawide + tablet */
|
/* Settings: 与顶栏资金卡同宽(由 .app-container 约束) */
|
||||||
.settings-page {
|
.settings-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-card {
|
.settings-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-card-narrow {
|
.settings-card-narrow {
|
||||||
max-width: 640px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-lead {
|
.settings-lead {
|
||||||
@@ -1280,15 +1281,39 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1100px) {
|
@media (min-width: 1100px) {
|
||||||
.settings-page {
|
|
||||||
max-width: 1320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-fields {
|
.settings-fields {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-xfer-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||||||
|
gap: 12px 14px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-xfer-row .field {
|
||||||
|
margin-bottom: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-xfer-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.settings-xfer-row {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-xfer-action {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 699px) {
|
@media (max-width: 699px) {
|
||||||
.settings-fields .field {
|
.settings-fields .field {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user