From 0b8e5a09141c18a61f7243f1a5bf499e33aa6953 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 7 Aug 2026 10:30:34 +0800 Subject: [PATCH] Fix hub funds overview double-counting OKX USDT with options. Only add options USDC/USDG onto perpetual USDT totals, repair historical double-counted snapshots, and label the options line as USDC. Co-authored-by: Cursor --- lib/hub/hub_fund_history_lib.py | 7 +- lib/hub/hub_options_funds_lib.py | 99 ++++++++++++++++++++++++---- manual_trading_hub/static/funds.js | 2 +- manual_trading_hub/static/index.html | 2 +- tests/test_hub_options_funds_lib.py | 43 +++++++++++- 5 files changed, 134 insertions(+), 19 deletions(-) diff --git a/lib/hub/hub_fund_history_lib.py b/lib/hub/hub_fund_history_lib.py index f38a3af..1f5888b 100644 --- a/lib/hub/hub_fund_history_lib.py +++ b/lib/hub/hub_fund_history_lib.py @@ -8,7 +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.hub.hub_options_funds_lib import merge_board_row_balances, repair_double_counted_fund_entry from lib.paths import manual_trading_hub_dir @@ -275,7 +275,7 @@ def _series_from_history( total = 0.0 n = 0 for key in account_keys: - ac = ac_map.get(key) or {} + ac = repair_double_counted_fund_entry(ac_map.get(key) or {}) t = account_total_usdt(ac.get("funding_usdt"), ac.get("trading_usdt")) if t is None: t = _safe_float(ac.get("total_usdt")) @@ -291,7 +291,8 @@ def _series_from_history( def _account_series(history: dict[str, dict], key: str) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for day in sorted(history.keys()): - ac = (history.get(day) or {}).get("accounts", {}).get(key) or {} + raw = (history.get(day) or {}).get("accounts", {}).get(key) or {} + ac = repair_double_counted_fund_entry(raw) t = account_total_usdt(ac.get("funding_usdt"), ac.get("trading_usdt")) if t is None: t = _safe_float(ac.get("total_usdt")) diff --git a/lib/hub/hub_options_funds_lib.py b/lib/hub/hub_options_funds_lib.py index 602d4ef..e466747 100644 --- a/lib/hub/hub_options_funds_lib.py +++ b/lib/hub/hub_options_funds_lib.py @@ -36,17 +36,41 @@ def _sum_optional(*values: Any) -> Optional[float]: def options_balances_usdt_equiv(options_snap: dict[str, Any] | None) -> dict[str, Any]: - """从期权 snapshot 提取资金户/交易户 USDT 等价余额.""" + """从期权 snapshot 提取资金户/交易户 USDT 等价余额. + + - funding_usdt / trading_usdt: USDT+USDC(+USDG) 全账户(勿与永续 USDT 再加总) + - funding_usdc_equiv / trading_usdc_equiv: 仅非 USDT 稳定币,可安全加到永续 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} + return { + "ok": False, + "funding_usdt": None, + "trading_usdt": None, + "funding_usdc_equiv": None, + "trading_usdc_equiv": None, + } if snap.get("ok") is False: - return {"ok": False, "funding_usdt": None, "trading_usdt": None} + return { + "ok": False, + "funding_usdt": None, + "trading_usdt": None, + "funding_usdc_equiv": None, + "trading_usdc_equiv": 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")) + funding_usdc_equiv = _sum_optional(bal.get("funding_usdc"), bal.get("funding_usdg")) + trading_usdc_equiv = _sum_optional(bal.get("trading_usdc"), bal.get("trading_usdg")) + funding = _sum_optional(bal.get("funding_usdt"), funding_usdc_equiv) + trading = _sum_optional(bal.get("trading_usdt"), trading_usdc_equiv) ok = funding is not None and trading is not None - return {"ok": ok, "funding_usdt": funding, "trading_usdt": trading} + return { + "ok": ok, + "funding_usdt": funding, + "trading_usdt": trading, + "funding_usdc_equiv": funding_usdc_equiv, + "trading_usdc_equiv": trading_usdc_equiv, + } def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[float]: @@ -96,19 +120,45 @@ def merge_perp_options_balances( perpetual_trading_usdt: Any, options_snap: dict[str, Any] | None, ) -> dict[str, Any]: - """永续 + 期权余额合并为中控 USDT 统计口径.""" + """永续 USDT + 期权非 USDT 稳定币合并为中控总资金(避免 OKX 同账户 USDT 双计). + + 与实例顶栏 total_funds_usdt(..., options_usdc, None, None) 口径一致: + 期权 snapshot 里的 USDT 与永续资金/交易户是同一钱包,只把 USDC/USDG 加上. + """ 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")) + # 展示用期权户:优先非 USDT 稳定币;若仅有 USDT 则仍给出全量以便辨识 + opt_fund_disp = opt.get("funding_usdc_equiv") + opt_trade_disp = opt.get("trading_usdc_equiv") + if opt_fund_disp is None and opt_trade_disp is None and opt.get("ok"): + opt_fund_disp = opt.get("funding_usdt") + opt_trade_disp = opt.get("trading_usdt") + + if opt.get("ok"): + if perpetual_funding_usdt is None and perpetual_trading_usdt is None: + # 永续账户未取到时,期权 snapshot 已含同账户 USDT+USDC,直接用全量 + funding = opt.get("funding_usdt") + trading = opt.get("trading_usdt") + else: + funding = _sum_optional(perpetual_funding_usdt, opt.get("funding_usdc_equiv")) + trading = _sum_optional(perpetual_trading_usdt, opt.get("trading_usdc_equiv")) + else: + funding = _safe_float(perpetual_funding_usdt) + trading = _safe_float(perpetual_trading_usdt) + total = _account_total_usdt(funding, trading) + # 任一侧齐全即可展示;永续缺一侧但有期权 USDC 时仍尽量给出合计 + if total is None: + total = _sum_optional(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_funding_usdt": opt_fund_disp, + "options_trading_usdt": opt_trade_disp, + "options_funding_full_usdt": opt.get("funding_usdt"), + "options_trading_full_usdt": opt.get("trading_usdt"), "options_ok": bool(opt.get("ok")), "funding_usdt": funding, "trading_usdt": trading, @@ -119,6 +169,33 @@ def merge_perp_options_balances( } +def repair_double_counted_fund_entry(ac: dict[str, Any]) -> dict[str, Any]: + """识别并修复历史快照中「永续 USDT + 期权(USDT+USDC)」的双计. + + 旧口径 options_* 存的是 USDT+USDC 全量,且 funding≈2×期权资金户 USDT 部分. + 新口径 options_* 多为纯 USDC,不会误伤. + """ + if not isinstance(ac, dict): + return {} + out = dict(ac) + ofu = _safe_float(ac.get("options_funding_usdt")) + otu = _safe_float(ac.get("options_trading_usdt")) + fu = _safe_float(ac.get("funding_usdt")) + tu = _safe_float(ac.get("trading_usdt")) + if ofu is None or otu is None or fu is None or tu is None: + return out + if ofu < 1.0: + return out + ratio = fu / ofu if ofu > 0 else 0.0 + # 经典双计:合并资金户 ≈ 2 × 期权资金户(同钱包 USDT 加了两遍) + if 1.8 <= ratio <= 2.25: + out["funding_usdt"] = ofu + out["trading_usdt"] = otu + out["total_usdt"] = round(ofu + otu, 4) + out["repaired_double_count"] = True + return out + + def merge_board_row_balances(row: dict[str, Any]) -> dict[str, Any]: """监控板行 → 含期权的资金统计.""" caps = row.get("capabilities") or [] diff --git a/manual_trading_hub/static/funds.js b/manual_trading_hub/static/funds.js index 8030121..29c77d9 100644 --- a/manual_trading_hub/static/funds.js +++ b/manual_trading_hub/static/funds.js @@ -211,7 +211,7 @@ monitored && ac.options_trading_usdt != null ? fmt(ac.options_trading_usdt, 2) + " U" : ""; const optLine = optFunding || optTrading - ? '
期权户' + + ? '
期权USDC' + (optFunding || "—") + " / " + (optTrading || "—") + diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 003ed4b..2e5808b 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -1756,7 +1756,7 @@ - + diff --git a/tests/test_hub_options_funds_lib.py b/tests/test_hub_options_funds_lib.py index 8f28fa3..323efd9 100644 --- a/tests/test_hub_options_funds_lib.py +++ b/tests/test_hub_options_funds_lib.py @@ -4,6 +4,7 @@ from lib.hub.hub_options_funds_lib import ( merge_board_row_balances, merge_perp_options_balances, options_balances_usdt_equiv, + repair_double_counted_fund_entry, ) @@ -18,20 +19,30 @@ class HubOptionsFundsLibTests(TestCase): self.assertTrue(out["ok"]) self.assertEqual(out["funding_usdt"], 10.0) self.assertEqual(out["trading_usdt"], 7.0) + self.assertEqual(out["funding_usdc_equiv"], 10.0) + self.assertEqual(out["trading_usdc_equiv"], 2.0) - def test_merge_perp_options_balances(self): + def test_merge_perp_options_balances_adds_usdc_only(self): + """永续已含同账户 USDT 时,只叠加期权 USDC,避免双计.""" out = merge_perp_options_balances( 100, 50, { "ok": True, "enabled": True, - "balances": {"funding_usdc": 8, "trading_usdc": 4}, + "balances": { + "funding_usdt": 100, + "funding_usdc": 8, + "trading_usdt": 50, + "trading_usdc": 4, + }, }, ) self.assertEqual(out["funding_usdt"], 108.0) self.assertEqual(out["trading_usdt"], 54.0) self.assertEqual(out["total_usdt"], 162.0) + self.assertEqual(out["options_funding_usdt"], 8.0) + self.assertEqual(out["options_trading_usdt"], 4.0) def test_merge_board_row_balances(self): row = { @@ -42,7 +53,7 @@ class HubOptionsFundsLibTests(TestCase): "options": { "ok": True, "enabled": True, - "balances": {"funding_usdc": 1, "trading_usdc": 2}, + "balances": {"funding_usdt": 20, "funding_usdc": 1, "trading_usdc": 2}, "positions": [{"inst_id": "X"}], "upl_total_usdc": 0.5, }, @@ -51,3 +62,29 @@ class HubOptionsFundsLibTests(TestCase): self.assertEqual(out["total_usdt"], 53.0) self.assertEqual(out["options_open_position_count"], 1) self.assertEqual(out["options_float_pnl_u"], 0.5) + + def test_repair_double_counted_fund_entry(self): + raw = { + "funding_usdt": 586.82, + "trading_usdt": 69.46, + "total_usdt": 656.28, + "options_funding_usdt": 293.26, + "options_trading_usdt": 69.44, + } + fixed = repair_double_counted_fund_entry(raw) + self.assertTrue(fixed.get("repaired_double_count")) + self.assertEqual(fixed["funding_usdt"], 293.26) + self.assertEqual(fixed["trading_usdt"], 69.44) + self.assertEqual(fixed["total_usdt"], round(293.26 + 69.44, 4)) + + def test_repair_skips_new_usdc_only_options_line(self): + raw = { + "funding_usdt": 293.26, + "trading_usdt": 69.44, + "total_usdt": 362.7, + "options_funding_usdt": 0.5, + "options_trading_usdt": 69.42, + } + fixed = repair_double_counted_fund_entry(raw) + self.assertFalse(fixed.get("repaired_double_count")) + self.assertEqual(fixed["total_usdt"], 362.7)