Hub funds and dashboard: include OKX options balances in USDT totals.

Merge options USDC/USDT at 1:1 into fund overview snapshots, dashboard aggregation, and monitor board totals while keeping a separate options breakdown in the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 13:06:01 +08:00
parent 6cc3382526
commit 3163912266
9 changed files with 362 additions and 21 deletions
+40 -12
View File
@@ -8,6 +8,7 @@ from pathlib import Path
from typing import Any, Optional
from lib.hub.hub_trades_lib import current_trading_day
from lib.hub.hub_options_funds_lib import merge_board_row_balances
from lib.paths import manual_trading_hub_dir
@@ -160,13 +161,20 @@ def record_fund_snapshot(
total = account_total_usdt(fu, tu)
if total is None:
continue
row_accounts[key] = {
entry: dict[str, Any] = {
"name": ac.get("name"),
"funding_usdt": fu,
"trading_usdt": tu,
"total_usdt": total,
"recorded_at": _now_str(),
}
ofu = _safe_float(ac.get("options_funding_usdt"))
otu = _safe_float(ac.get("options_trading_usdt"))
if ofu is not None:
entry["options_funding_usdt"] = ofu
if otu is not None:
entry["options_trading_usdt"] = otu
row_accounts[key] = entry
if row_accounts:
days[day] = {"accounts": row_accounts, "updated_at": _now_str()}
days = _prune_days(
@@ -188,14 +196,23 @@ def record_fund_snapshot_from_board(
for row in rows or []:
if not isinstance(row, dict):
continue
if not row.get("account_ok"):
if not row.get("account_ok") and not (
"options" in (row.get("capabilities") or [])
and isinstance(row.get("options"), dict)
and row.get("options", {}).get("ok")
):
continue
merged = merge_board_row_balances(row)
if not merged.get("data_ok"):
continue
accounts.append(
{
"key": row.get("key") or row.get("id"),
"name": row.get("name"),
"funding_usdt": row.get("funding_usdt"),
"trading_usdt": row.get("trading_usdt"),
"funding_usdt": merged.get("funding_usdt"),
"trading_usdt": merged.get("trading_usdt"),
"options_funding_usdt": merged.get("options_funding_usdt"),
"options_trading_usdt": merged.get("options_trading_usdt"),
"monitored": True,
}
)
@@ -297,14 +314,21 @@ def build_fund_overview(
monitored = True
row = _live_row_for_exchange(ex, rows_by_key)
fu = tu = total = None
pf = pt = ofu = otu = None
data_ok = False
if row and row.get("account_ok"):
fu = _safe_float(row.get("funding_usdt"))
tu = _safe_float(row.get("trading_usdt"))
total = account_total_usdt(fu, tu)
data_ok = total is not None
if data_ok:
live_total += total
caps = ex.get("capabilities") or []
if row:
merged = merge_board_row_balances({**row, "capabilities": caps})
if merged.get("data_ok"):
fu = merged.get("funding_usdt")
tu = merged.get("trading_usdt")
total = merged.get("total_usdt")
pf = merged.get("perpetual_funding_usdt")
pt = merged.get("perpetual_trading_usdt")
ofu = merged.get("options_funding_usdt")
otu = merged.get("options_trading_usdt")
data_ok = True
live_total += float(total)
live_known += 1
series = _account_series(history, key) if key else []
@@ -329,6 +353,10 @@ def build_fund_overview(
"data_ok": data_ok,
"funding_usdt": fu,
"trading_usdt": tu,
"perpetual_funding_usdt": pf,
"perpetual_trading_usdt": pt,
"options_funding_usdt": ofu,
"options_trading_usdt": otu,
"total_usdt": total,
"series": series,
"drawdown": dd,
@@ -387,7 +415,7 @@ def format_fund_history_text(
if not history:
return "(暂无资金历史快照)"
names = account_names or {}
lines = ["【资金快照(资金账户 + 交易账户 USDT)】"]
lines = ["【资金快照(资金账户 + 交易账户 USDT,含期权 USDC≈USDT)】"]
for day in sorted(history.keys()):
block = history.get(day) or {}
ac_map = block.get("accounts") or {}
+15
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any
from lib.hub.hub_options_funds_lib import options_float_pnl_usdt, options_open_position_count
def _coerce_float(value: Any) -> float | None:
if value is None or value == "":
@@ -54,7 +56,9 @@ def aggregate_monitor_board_totals(
win_pnl_u = 0.0
loss_pnl_u = 0.0
open_position_count = 0
options_open_position_count = 0
float_pnl_u = 0.0
options_float_pnl_u = 0.0
for row in rows or []:
if not isinstance(row, dict):
@@ -78,6 +82,15 @@ def aggregate_monitor_board_totals(
else:
float_pnl_u += sum(position_unrealized_pnl(p) for p in open_pos)
opt_snap = row.get("options") if "options" in (row.get("capabilities") or []) else None
opt_count = options_open_position_count(opt_snap)
options_open_position_count += opt_count
open_position_count += opt_count
opt_upl = options_float_pnl_usdt(opt_snap)
if opt_upl is not None:
options_float_pnl_u += opt_upl
float_pnl_u += opt_upl
return {
"trading_day": trading_day,
"reset_hour": int(reset_hour),
@@ -89,5 +102,7 @@ def aggregate_monitor_board_totals(
"loss_pnl_u": round(loss_pnl_u, 4),
"realized_pnl_u": round(win_pnl_u + loss_pnl_u, 4),
"open_position_count": open_position_count,
"options_open_position_count": options_open_position_count,
"float_pnl_u": round(float_pnl_u, 4),
"options_float_pnl_u": round(options_float_pnl_u, 4),
}
+117
View File
@@ -0,0 +1,117 @@
"""中控资金统计:期权 USDC/USDT 按 1:1 计入 USDT 合计。"""
from __future__ import annotations
from typing import Any, Optional
def _safe_float(value: Any) -> Optional[float]:
try:
if value is None or value == "":
return None
v = float(value)
return v if v >= 0 else None
except (TypeError, ValueError):
return None
def _account_total_usdt(funding: Any, trading: Any) -> Optional[float]:
fu = _safe_float(funding)
tu = _safe_float(trading)
if fu is None or tu is None:
return None
return round(fu + tu, 4)
def stablecoin_usdt_equiv(value: Any) -> Optional[float]:
"""USDC / USDT 按 1:1 折算为 USDT 统计口径。"""
return _safe_float(value)
def _sum_optional(*values: Any) -> Optional[float]:
parts = [_safe_float(v) for v in values]
present = [p for p in parts if p is not None]
if not present:
return None
return round(sum(present), 4)
def options_balances_usdt_equiv(options_snap: dict[str, Any] | None) -> dict[str, Any]:
"""从期权 snapshot 提取资金户/交易户 USDT 等价余额。"""
snap = options_snap if isinstance(options_snap, dict) else {}
if snap.get("enabled") is False:
return {"ok": False, "funding_usdt": None, "trading_usdt": None}
if snap.get("ok") is False:
return {"ok": False, "funding_usdt": None, "trading_usdt": None}
bal = snap.get("balances") if isinstance(snap.get("balances"), dict) else snap
funding = _sum_optional(bal.get("funding_usdt"), bal.get("funding_usdc"))
trading = _sum_optional(bal.get("trading_usdt"), bal.get("trading_usdc"))
ok = funding is not None and trading is not None
return {"ok": ok, "funding_usdt": funding, "trading_usdt": trading}
def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[float]:
snap = options_snap if isinstance(options_snap, dict) else {}
if snap.get("enabled") is False or snap.get("ok") is False:
return None
upl = snap.get("upl_total_usdc")
if upl is None:
return None
try:
return round(float(upl), 4)
except (TypeError, ValueError):
return None
def options_open_position_count(options_snap: dict[str, Any] | None) -> int:
snap = options_snap if isinstance(options_snap, dict) else {}
if snap.get("enabled") is False or snap.get("ok") is False:
return 0
if snap.get("position_count") is not None:
try:
return max(0, int(snap.get("position_count")))
except (TypeError, ValueError):
pass
pos = snap.get("positions")
return len(pos) if isinstance(pos, list) else 0
def merge_perp_options_balances(
perpetual_funding_usdt: Any,
perpetual_trading_usdt: Any,
options_snap: dict[str, Any] | None,
) -> dict[str, Any]:
"""永续 + 期权余额合并为中控 USDT 统计口径。"""
opt = options_balances_usdt_equiv(options_snap)
funding = _sum_optional(perpetual_funding_usdt, opt.get("funding_usdt"))
trading = _sum_optional(perpetual_trading_usdt, opt.get("trading_usdt"))
total = _account_total_usdt(funding, trading)
perp_total = _account_total_usdt(perpetual_funding_usdt, perpetual_trading_usdt)
opt_total = _account_total_usdt(opt.get("funding_usdt"), opt.get("trading_usdt"))
data_ok = total is not None
return {
"perpetual_funding_usdt": _safe_float(perpetual_funding_usdt),
"perpetual_trading_usdt": _safe_float(perpetual_trading_usdt),
"options_funding_usdt": opt.get("funding_usdt"),
"options_trading_usdt": opt.get("trading_usdt"),
"options_ok": bool(opt.get("ok")),
"funding_usdt": funding,
"trading_usdt": trading,
"total_usdt": total,
"perpetual_total_usdt": perp_total,
"options_total_usdt": opt_total,
"data_ok": data_ok,
}
def merge_board_row_balances(row: dict[str, Any]) -> dict[str, Any]:
"""监控板行 → 含期权的资金统计。"""
caps = row.get("capabilities") or []
options_snap = row.get("options") if "options" in caps else None
merged = merge_perp_options_balances(
row.get("funding_usdt") if row.get("account_ok") else None,
row.get("trading_usdt") if row.get("account_ok") else None,
options_snap,
)
merged["options_float_pnl_u"] = options_float_pnl_usdt(options_snap)
merged["options_open_position_count"] = options_open_position_count(options_snap)
return merged