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:
@@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from lib.hub.hub_trades_lib import current_trading_day
|
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
|
from lib.paths import manual_trading_hub_dir
|
||||||
|
|
||||||
@@ -160,13 +161,20 @@ def record_fund_snapshot(
|
|||||||
total = account_total_usdt(fu, tu)
|
total = account_total_usdt(fu, tu)
|
||||||
if total is None:
|
if total is None:
|
||||||
continue
|
continue
|
||||||
row_accounts[key] = {
|
entry: dict[str, Any] = {
|
||||||
"name": ac.get("name"),
|
"name": ac.get("name"),
|
||||||
"funding_usdt": fu,
|
"funding_usdt": fu,
|
||||||
"trading_usdt": tu,
|
"trading_usdt": tu,
|
||||||
"total_usdt": total,
|
"total_usdt": total,
|
||||||
"recorded_at": _now_str(),
|
"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:
|
if row_accounts:
|
||||||
days[day] = {"accounts": row_accounts, "updated_at": _now_str()}
|
days[day] = {"accounts": row_accounts, "updated_at": _now_str()}
|
||||||
days = _prune_days(
|
days = _prune_days(
|
||||||
@@ -188,14 +196,23 @@ def record_fund_snapshot_from_board(
|
|||||||
for row in rows or []:
|
for row in rows or []:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
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
|
continue
|
||||||
accounts.append(
|
accounts.append(
|
||||||
{
|
{
|
||||||
"key": row.get("key") or row.get("id"),
|
"key": row.get("key") or row.get("id"),
|
||||||
"name": row.get("name"),
|
"name": row.get("name"),
|
||||||
"funding_usdt": row.get("funding_usdt"),
|
"funding_usdt": merged.get("funding_usdt"),
|
||||||
"trading_usdt": row.get("trading_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,
|
"monitored": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -297,14 +314,21 @@ def build_fund_overview(
|
|||||||
monitored = True
|
monitored = True
|
||||||
row = _live_row_for_exchange(ex, rows_by_key)
|
row = _live_row_for_exchange(ex, rows_by_key)
|
||||||
fu = tu = total = None
|
fu = tu = total = None
|
||||||
|
pf = pt = ofu = otu = None
|
||||||
data_ok = False
|
data_ok = False
|
||||||
if row and row.get("account_ok"):
|
caps = ex.get("capabilities") or []
|
||||||
fu = _safe_float(row.get("funding_usdt"))
|
if row:
|
||||||
tu = _safe_float(row.get("trading_usdt"))
|
merged = merge_board_row_balances({**row, "capabilities": caps})
|
||||||
total = account_total_usdt(fu, tu)
|
if merged.get("data_ok"):
|
||||||
data_ok = total is not None
|
fu = merged.get("funding_usdt")
|
||||||
if data_ok:
|
tu = merged.get("trading_usdt")
|
||||||
live_total += total
|
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
|
live_known += 1
|
||||||
|
|
||||||
series = _account_series(history, key) if key else []
|
series = _account_series(history, key) if key else []
|
||||||
@@ -329,6 +353,10 @@ def build_fund_overview(
|
|||||||
"data_ok": data_ok,
|
"data_ok": data_ok,
|
||||||
"funding_usdt": fu,
|
"funding_usdt": fu,
|
||||||
"trading_usdt": tu,
|
"trading_usdt": tu,
|
||||||
|
"perpetual_funding_usdt": pf,
|
||||||
|
"perpetual_trading_usdt": pt,
|
||||||
|
"options_funding_usdt": ofu,
|
||||||
|
"options_trading_usdt": otu,
|
||||||
"total_usdt": total,
|
"total_usdt": total,
|
||||||
"series": series,
|
"series": series,
|
||||||
"drawdown": dd,
|
"drawdown": dd,
|
||||||
@@ -387,7 +415,7 @@ def format_fund_history_text(
|
|||||||
if not history:
|
if not history:
|
||||||
return "(暂无资金历史快照)"
|
return "(暂无资金历史快照)"
|
||||||
names = account_names or {}
|
names = account_names or {}
|
||||||
lines = ["【资金快照(资金账户 + 交易账户 USDT)】"]
|
lines = ["【资金快照(资金账户 + 交易账户 USDT,含期权 USDC≈USDT)】"]
|
||||||
for day in sorted(history.keys()):
|
for day in sorted(history.keys()):
|
||||||
block = history.get(day) or {}
|
block = history.get(day) or {}
|
||||||
ac_map = block.get("accounts") or {}
|
ac_map = block.get("accounts") or {}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
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:
|
def _coerce_float(value: Any) -> float | None:
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
@@ -54,7 +56,9 @@ def aggregate_monitor_board_totals(
|
|||||||
win_pnl_u = 0.0
|
win_pnl_u = 0.0
|
||||||
loss_pnl_u = 0.0
|
loss_pnl_u = 0.0
|
||||||
open_position_count = 0
|
open_position_count = 0
|
||||||
|
options_open_position_count = 0
|
||||||
float_pnl_u = 0.0
|
float_pnl_u = 0.0
|
||||||
|
options_float_pnl_u = 0.0
|
||||||
|
|
||||||
for row in rows or []:
|
for row in rows or []:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
@@ -78,6 +82,15 @@ def aggregate_monitor_board_totals(
|
|||||||
else:
|
else:
|
||||||
float_pnl_u += sum(position_unrealized_pnl(p) for p in open_pos)
|
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 {
|
return {
|
||||||
"trading_day": trading_day,
|
"trading_day": trading_day,
|
||||||
"reset_hour": int(reset_hour),
|
"reset_hour": int(reset_hour),
|
||||||
@@ -89,5 +102,7 @@ def aggregate_monitor_board_totals(
|
|||||||
"loss_pnl_u": round(loss_pnl_u, 4),
|
"loss_pnl_u": round(loss_pnl_u, 4),
|
||||||
"realized_pnl_u": round(win_pnl_u + loss_pnl_u, 4),
|
"realized_pnl_u": round(win_pnl_u + loss_pnl_u, 4),
|
||||||
"open_position_count": open_position_count,
|
"open_position_count": open_position_count,
|
||||||
|
"options_open_position_count": options_open_position_count,
|
||||||
"float_pnl_u": round(float_pnl_u, 4),
|
"float_pnl_u": round(float_pnl_u, 4),
|
||||||
|
"options_float_pnl_u": round(options_float_pnl_u, 4),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,6 +21,11 @@ from hub_ai.config import (
|
|||||||
trading_day_reset_hour,
|
trading_day_reset_hour,
|
||||||
)
|
)
|
||||||
from hub_ai.fund_history import format_fund_history_text, get_fund_history, record_fund_snapshot
|
from hub_ai.fund_history import format_fund_history_text, get_fund_history, record_fund_snapshot
|
||||||
|
from lib.hub.hub_options_funds_lib import (
|
||||||
|
merge_perp_options_balances,
|
||||||
|
options_float_pnl_usdt,
|
||||||
|
options_open_position_count,
|
||||||
|
)
|
||||||
from lib.hub.hub_trades_lib import current_trading_day, summarize_trades
|
from lib.hub.hub_trades_lib import current_trading_day, summarize_trades
|
||||||
|
|
||||||
_CHAT_CONTEXT_CACHE: dict[str, dict[str, Any]] = {}
|
_CHAT_CONTEXT_CACHE: dict[str, dict[str, Any]] = {}
|
||||||
@@ -413,6 +418,13 @@ def _fetch_account_bundle(
|
|||||||
"funding_usdt": None,
|
"funding_usdt": None,
|
||||||
"trading_usdt": None,
|
"trading_usdt": None,
|
||||||
"available_trading_usdt": None,
|
"available_trading_usdt": None,
|
||||||
|
"perpetual_funding_usdt": None,
|
||||||
|
"perpetual_trading_usdt": None,
|
||||||
|
"options_funding_usdt": None,
|
||||||
|
"options_trading_usdt": None,
|
||||||
|
"options_float_pnl_u": None,
|
||||||
|
"options_open_position_count": 0,
|
||||||
|
"options_snapshot": None,
|
||||||
"trades_yesterday": [],
|
"trades_yesterday": [],
|
||||||
"trade_stats_yesterday": summarize_trades([]),
|
"trade_stats_yesterday": summarize_trades([]),
|
||||||
"monitor_lines": {"trends": [], "orders": [], "keys": [], "rolls": []},
|
"monitor_lines": {"trends": [], "orders": [], "keys": [], "rolls": []},
|
||||||
@@ -465,8 +477,10 @@ def _fetch_account_bundle(
|
|||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
acct_body = r.json()
|
acct_body = r.json()
|
||||||
if isinstance(acct_body, dict) and acct_body.get("ok"):
|
if isinstance(acct_body, dict) and acct_body.get("ok"):
|
||||||
base["funding_usdt"] = _safe_float(acct_body.get("funding_usdt"))
|
base["perpetual_funding_usdt"] = _safe_float(acct_body.get("funding_usdt"))
|
||||||
base["trading_usdt"] = _safe_float(acct_body.get("trading_usdt"))
|
base["perpetual_trading_usdt"] = _safe_float(acct_body.get("trading_usdt"))
|
||||||
|
base["funding_usdt"] = base["perpetual_funding_usdt"]
|
||||||
|
base["trading_usdt"] = base["perpetual_trading_usdt"]
|
||||||
base["available_trading_usdt"] = _safe_float(acct_body.get("available_trading_usdt"))
|
base["available_trading_usdt"] = _safe_float(acct_body.get("available_trading_usdt"))
|
||||||
base["flask_ok"] = True
|
base["flask_ok"] = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -542,6 +556,41 @@ def _fetch_account_bundle(
|
|||||||
if base["positions"]:
|
if base["positions"]:
|
||||||
_enrich_positions_exchange_tpsl(base["positions"], price_snap, hub_mon)
|
_enrich_positions_exchange_tpsl(base["positions"], price_snap, hub_mon)
|
||||||
|
|
||||||
|
caps = ex.get("capabilities") or []
|
||||||
|
if "options" in caps:
|
||||||
|
try:
|
||||||
|
r = client.get(
|
||||||
|
f"{flask_url}/api/hub/options/snapshot",
|
||||||
|
headers=_hub_headers(),
|
||||||
|
timeout=hub_flask_timeout(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
opt_body = r.json()
|
||||||
|
if isinstance(opt_body, dict):
|
||||||
|
base["options_snapshot"] = opt_body
|
||||||
|
if opt_body.get("ok") is not False and opt_body.get("enabled") is not False:
|
||||||
|
base["flask_ok"] = True
|
||||||
|
merged = merge_perp_options_balances(
|
||||||
|
base.get("perpetual_funding_usdt"),
|
||||||
|
base.get("perpetual_trading_usdt"),
|
||||||
|
opt_body,
|
||||||
|
)
|
||||||
|
base["options_funding_usdt"] = merged.get("options_funding_usdt")
|
||||||
|
base["options_trading_usdt"] = merged.get("options_trading_usdt")
|
||||||
|
if merged.get("funding_usdt") is not None:
|
||||||
|
base["funding_usdt"] = merged.get("funding_usdt")
|
||||||
|
if merged.get("trading_usdt") is not None:
|
||||||
|
base["trading_usdt"] = merged.get("trading_usdt")
|
||||||
|
opt_count = options_open_position_count(opt_body)
|
||||||
|
base["options_open_position_count"] = opt_count
|
||||||
|
base["open_position_count"] += opt_count
|
||||||
|
opt_upl = options_float_pnl_usdt(opt_body)
|
||||||
|
if opt_upl is not None:
|
||||||
|
base["options_float_pnl_u"] = opt_upl
|
||||||
|
base["float_pnl_u"] = round(float(base["float_pnl_u"]) + opt_upl, 4)
|
||||||
|
except Exception as exc:
|
||||||
|
base["issues"].append(f"期权接口: {exc}")
|
||||||
|
|
||||||
if monitored and not base["agent_ok"] and not base["flask_ok"]:
|
if monitored and not base["agent_ok"] and not base["flask_ok"]:
|
||||||
base["status"] = "连接异常"
|
base["status"] = "连接异常"
|
||||||
elif base["issues"]:
|
elif base["issues"]:
|
||||||
@@ -598,6 +647,9 @@ def build_daily_context(
|
|||||||
total_funding = 0.0
|
total_funding = 0.0
|
||||||
total_trading = 0.0
|
total_trading = 0.0
|
||||||
total_open_positions = 0
|
total_open_positions = 0
|
||||||
|
total_options_open_positions = 0
|
||||||
|
total_options_float = 0.0
|
||||||
|
options_float_known = 0
|
||||||
funding_known = trading_known = 0
|
funding_known = trading_known = 0
|
||||||
for ac in accounts:
|
for ac in accounts:
|
||||||
if ac.get("status") == "未监控":
|
if ac.get("status") == "未监控":
|
||||||
@@ -609,6 +661,11 @@ def build_daily_context(
|
|||||||
total_loss += int(st.get("loss_count") or 0)
|
total_loss += int(st.get("loss_count") or 0)
|
||||||
total_float += float(ac.get("float_pnl_u") or 0)
|
total_float += float(ac.get("float_pnl_u") or 0)
|
||||||
total_open_positions += int(ac.get("open_position_count") or _account_open_position_count(ac))
|
total_open_positions += int(ac.get("open_position_count") or _account_open_position_count(ac))
|
||||||
|
total_options_open_positions += int(ac.get("options_open_position_count") or 0)
|
||||||
|
opt_float = _safe_float(ac.get("options_float_pnl_u"))
|
||||||
|
if opt_float is not None:
|
||||||
|
total_options_float += opt_float
|
||||||
|
options_float_known += 1
|
||||||
fu = _safe_float(ac.get("funding_usdt"))
|
fu = _safe_float(ac.get("funding_usdt"))
|
||||||
tu = _safe_float(ac.get("trading_usdt"))
|
tu = _safe_float(ac.get("trading_usdt"))
|
||||||
if fu is not None:
|
if fu is not None:
|
||||||
@@ -631,6 +688,8 @@ def build_daily_context(
|
|||||||
"loss_count": total_loss,
|
"loss_count": total_loss,
|
||||||
"float_pnl_u": round(total_float, 4),
|
"float_pnl_u": round(total_float, 4),
|
||||||
"open_position_count": total_open_positions,
|
"open_position_count": total_open_positions,
|
||||||
|
"options_open_position_count": total_options_open_positions,
|
||||||
|
"options_float_pnl_u": round(total_options_float, 4) if options_float_known else None,
|
||||||
"total_funding_usdt": round(total_funding, 4) if total_funding is not None else None,
|
"total_funding_usdt": round(total_funding, 4) if total_funding is not None else None,
|
||||||
"total_trading_usdt": round(total_trading, 4) if total_trading is not None else None,
|
"total_trading_usdt": round(total_trading, 4) if total_trading is not None else None,
|
||||||
}
|
}
|
||||||
@@ -929,6 +988,25 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
|
|||||||
"pnl": round(upnl, 4),
|
"pnl": round(upnl, 4),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
opt_snap = ac.get("options_snapshot") if isinstance(ac.get("options_snapshot"), dict) else {}
|
||||||
|
if opt_snap.get("ok") is not False and opt_snap.get("enabled") is not False:
|
||||||
|
for p in opt_snap.get("positions") or []:
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
continue
|
||||||
|
inst = p.get("inst_id") or "?"
|
||||||
|
opt_type = (p.get("opt_type") or "").upper()
|
||||||
|
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else opt_type or "OPT"
|
||||||
|
upl = p.get("upl")
|
||||||
|
line: dict[str, Any] = {
|
||||||
|
"kind": "options",
|
||||||
|
"text": f"期权 {inst} {label}",
|
||||||
|
}
|
||||||
|
if upl is not None:
|
||||||
|
try:
|
||||||
|
line["pnl"] = round(float(upl), 4)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
position_lines.append(line)
|
||||||
issues = [str(x) for x in (ac.get("issues") or [])[:3]]
|
issues = [str(x) for x in (ac.get("issues") or [])[:3]]
|
||||||
return {
|
return {
|
||||||
"monitor_counts": {
|
"monitor_counts": {
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ def _enrich_account_row(ac: dict) -> dict:
|
|||||||
"monitored": ac.get("status") != "未监控",
|
"monitored": ac.get("status") != "未监控",
|
||||||
"funding_usdt": ac.get("funding_usdt"),
|
"funding_usdt": ac.get("funding_usdt"),
|
||||||
"trading_usdt": ac.get("trading_usdt"),
|
"trading_usdt": ac.get("trading_usdt"),
|
||||||
|
"perpetual_funding_usdt": ac.get("perpetual_funding_usdt"),
|
||||||
|
"perpetual_trading_usdt": ac.get("perpetual_trading_usdt"),
|
||||||
|
"options_funding_usdt": ac.get("options_funding_usdt"),
|
||||||
|
"options_trading_usdt": ac.get("options_trading_usdt"),
|
||||||
|
"options_float_pnl_u": ac.get("options_float_pnl_u"),
|
||||||
|
"options_open_position_count": ac.get("options_open_position_count"),
|
||||||
"capital_total_usdt": round(capital, 4) if capital is not None else None,
|
"capital_total_usdt": round(capital, 4) if capital is not None else None,
|
||||||
"available_trading_usdt": ac.get("available_trading_usdt"),
|
"available_trading_usdt": ac.get("available_trading_usdt"),
|
||||||
"pnl_u": st.get("total_pnl_u"),
|
"pnl_u": st.get("total_pnl_u"),
|
||||||
|
|||||||
@@ -60,6 +60,8 @@
|
|||||||
const floating = Number(totals.float_pnl_u);
|
const floating = Number(totals.float_pnl_u);
|
||||||
const funding = totals.total_funding_usdt;
|
const funding = totals.total_funding_usdt;
|
||||||
const trading = totals.total_trading_usdt;
|
const trading = totals.total_trading_usdt;
|
||||||
|
const optionsFloat = Number(totals.options_float_pnl_u);
|
||||||
|
const optionsPos = totals.options_open_position_count;
|
||||||
elKpi.innerHTML = [
|
elKpi.innerHTML = [
|
||||||
kpiCard("交易日", esc(totals.trading_day || "—"), ""),
|
kpiCard("交易日", esc(totals.trading_day || "—"), ""),
|
||||||
kpiCard("平仓盈亏", pnlSigned(closed, 2), pnlClass(closed)),
|
kpiCard("平仓盈亏", pnlSigned(closed, 2), pnlClass(closed)),
|
||||||
@@ -76,10 +78,20 @@
|
|||||||
? `${fmt(Number(funding) + Number(trading), 2)}U`
|
? `${fmt(Number(funding) + Number(trading), 2)}U`
|
||||||
: "—",
|
: "—",
|
||||||
"",
|
"",
|
||||||
`资金 ${fmt(funding, 2)} + 交易 ${fmt(trading, 2)}`
|
`永续+期权 USDT 等价 · 资金 ${fmt(funding, 2)} + 交易 ${fmt(trading, 2)}`
|
||||||
),
|
),
|
||||||
kpiCard("实盘持仓", `${totals.open_position_count || 0} 仓`, ""),
|
kpiCard(
|
||||||
].join("");
|
"实盘持仓",
|
||||||
|
`${totals.open_position_count || 0} 仓`,
|
||||||
|
"",
|
||||||
|
Number.isFinite(optionsPos) && optionsPos > 0 ? `含期权 ${optionsPos} 仓` : ""
|
||||||
|
),
|
||||||
|
Number.isFinite(optionsFloat) && Math.abs(optionsFloat) > 1e-9
|
||||||
|
? kpiCard("期权浮盈", pnlSigned(optionsFloat, 2), pnlClass(optionsFloat))
|
||||||
|
: "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function kpiCard(label, value, valCls, sub) {
|
function kpiCard(label, value, valCls, sub) {
|
||||||
@@ -183,6 +195,24 @@
|
|||||||
alert && barW > 0
|
alert && barW > 0
|
||||||
? `<div class="dash-loss-bar" title="占资金合计 ${fmt(lossPct, 2)}%"><i style="width:${barW}%"></i></div>`
|
? `<div class="dash-loss-bar" title="占资金合计 ${fmt(lossPct, 2)}%"><i style="width:${barW}%"></i></div>`
|
||||||
: "";
|
: "";
|
||||||
|
const optFunding = ac.options_funding_usdt;
|
||||||
|
const optTrading = ac.options_trading_usdt;
|
||||||
|
const optFloat = Number(ac.options_float_pnl_u);
|
||||||
|
const optPos = Number(ac.options_open_position_count) || 0;
|
||||||
|
const optMetrics =
|
||||||
|
optFunding != null || optTrading != null || optPos > 0
|
||||||
|
? `<div class="dash-ac-metric"><span>期权资金</span><strong>${fmt(
|
||||||
|
optFunding != null && optTrading != null
|
||||||
|
? Number(optFunding) + Number(optTrading)
|
||||||
|
: null,
|
||||||
|
2
|
||||||
|
)}U</strong></div>
|
||||||
|
<div class="dash-ac-metric"><span>期权持仓</span><strong>${optPos} 仓</strong></div>
|
||||||
|
<div class="dash-ac-metric"><span>期权浮盈</span><strong class="${pnlClass(optFloat)}">${pnlSigned(
|
||||||
|
optFloat,
|
||||||
|
2
|
||||||
|
)}</strong></div>`
|
||||||
|
: "";
|
||||||
return `<article class="dash-ac-card${alert ? " is-alert" : ""}${unmon ? " is-unmon" : ""}">
|
return `<article class="dash-ac-card${alert ? " is-alert" : ""}${unmon ? " is-unmon" : ""}">
|
||||||
<div class="dash-ac-top">
|
<div class="dash-ac-top">
|
||||||
<div class="dash-ac-name">${esc(ac.name || "—")}</div>
|
<div class="dash-ac-name">${esc(ac.name || "—")}</div>
|
||||||
@@ -195,6 +225,7 @@
|
|||||||
<div class="dash-ac-metric"><span>今日盈亏</span><strong class="${pnlClass(pnl)}">${pnlSigned(pnl, 2)}</strong></div>
|
<div class="dash-ac-metric"><span>今日盈亏</span><strong class="${pnlClass(pnl)}">${pnlSigned(pnl, 2)}</strong></div>
|
||||||
<div class="dash-ac-metric"><span>平仓笔数</span><strong>${Number(ac.closed_count) || 0}</strong></div>
|
<div class="dash-ac-metric"><span>平仓笔数</span><strong>${Number(ac.closed_count) || 0}</strong></div>
|
||||||
<div class="dash-ac-metric"><span>浮盈亏</span><strong class="${pnlClass(floatPnl)}">${pnlSigned(floatPnl, 2)}</strong></div>
|
<div class="dash-ac-metric"><span>浮盈亏</span><strong class="${pnlClass(floatPnl)}">${pnlSigned(floatPnl, 2)}</strong></div>
|
||||||
|
${optMetrics}
|
||||||
</div>
|
</div>
|
||||||
${lossBar}
|
${lossBar}
|
||||||
${renderAccountDetail(ac)}
|
${renderAccountDetail(ac)}
|
||||||
|
|||||||
@@ -176,6 +176,18 @@
|
|||||||
monitored && ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) + " U" : "—";
|
monitored && ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) + " U" : "—";
|
||||||
const trading =
|
const trading =
|
||||||
monitored && ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) + " U" : "—";
|
monitored && ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) + " U" : "—";
|
||||||
|
const optFunding =
|
||||||
|
monitored && ac.options_funding_usdt != null ? fmt(ac.options_funding_usdt, 2) + " U" : "";
|
||||||
|
const optTrading =
|
||||||
|
monitored && ac.options_trading_usdt != null ? fmt(ac.options_trading_usdt, 2) + " U" : "";
|
||||||
|
const optLine =
|
||||||
|
optFunding || optTrading
|
||||||
|
? '<div><span class="k">期权户</span><span class="v">' +
|
||||||
|
(optFunding || "—") +
|
||||||
|
" / " +
|
||||||
|
(optTrading || "—") +
|
||||||
|
"</span></div>"
|
||||||
|
: "";
|
||||||
const dd = ac.drawdown || {};
|
const dd = ac.drawdown || {};
|
||||||
const ddU = dd.max_drawdown_u != null ? fmt(dd.max_drawdown_u, 2) + " U" : "—";
|
const ddU = dd.max_drawdown_u != null ? fmt(dd.max_drawdown_u, 2) + " U" : "—";
|
||||||
const ddPct = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—";
|
const ddPct = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—";
|
||||||
@@ -214,6 +226,7 @@
|
|||||||
'<div><span class="k">交易户</span><span class="v">' +
|
'<div><span class="k">交易户</span><span class="v">' +
|
||||||
trading +
|
trading +
|
||||||
"</span></div>" +
|
"</span></div>" +
|
||||||
|
optLine +
|
||||||
'<div><span class="k">较昨日</span><span class="v ' +
|
'<div><span class="k">较昨日</span><span class="v ' +
|
||||||
deltaCls +
|
deltaCls +
|
||||||
'">' +
|
'">' +
|
||||||
@@ -266,7 +279,7 @@
|
|||||||
if (elFsTitle) elFsTitle.textContent = ac.name || ac.key || "—";
|
if (elFsTitle) elFsTitle.textContent = ac.name || ac.key || "—";
|
||||||
if (elFsSub) {
|
if (elFsSub) {
|
||||||
const parts = [
|
const parts = [
|
||||||
"资金户 + 交易户(不含浮盈)",
|
"资金户 + 交易户 + 期权户(USDC≈USDT,不含浮盈)",
|
||||||
"交易日 " + (meta.trading_day || "—"),
|
"交易日 " + (meta.trading_day || "—"),
|
||||||
"自 " + (meta.history_start_day || "—") + " 起",
|
"自 " + (meta.history_start_day || "—") + " 起",
|
||||||
];
|
];
|
||||||
@@ -327,7 +340,7 @@
|
|||||||
const hour = data && data.reset_hour != null ? data.reset_hour : 8;
|
const hour = data && data.reset_hour != null ? data.reset_hour : 8;
|
||||||
if (elDescBody) {
|
if (elDescBody) {
|
||||||
elDescBody.textContent =
|
elDescBody.textContent =
|
||||||
"总资金 = 各监控户(资金账户 + 交易账户);自 " +
|
"总资金 = 各监控户(永续资金账户 + 交易账户 + 期权账户,USDC 按 1:1 计入 USDT);自 " +
|
||||||
start +
|
start +
|
||||||
" 起按北京时间 " +
|
" 起按北京时间 " +
|
||||||
hour +
|
hour +
|
||||||
|
|||||||
@@ -1145,8 +1145,8 @@
|
|||||||
<script src="/assets/calculator.js?v=3"></script>
|
<script src="/assets/calculator.js?v=3"></script>
|
||||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||||
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
|
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
|
||||||
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
||||||
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
<script src="/assets/dashboard.js?v=20260707-hub-options-funds"></script>
|
||||||
<script src="/assets/strategy.js?v=3"></script>
|
<script src="/assets/strategy.js?v=3"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from lib.hub.hub_options_funds_lib import (
|
||||||
|
merge_board_row_balances,
|
||||||
|
merge_perp_options_balances,
|
||||||
|
options_balances_usdt_equiv,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HubOptionsFundsLibTests(TestCase):
|
||||||
|
def test_options_balances_usdt_equiv(self):
|
||||||
|
snap = {
|
||||||
|
"ok": True,
|
||||||
|
"enabled": True,
|
||||||
|
"balances": {"funding_usdc": 10, "trading_usdt": 5, "trading_usdc": 2},
|
||||||
|
}
|
||||||
|
out = options_balances_usdt_equiv(snap)
|
||||||
|
self.assertTrue(out["ok"])
|
||||||
|
self.assertEqual(out["funding_usdt"], 10.0)
|
||||||
|
self.assertEqual(out["trading_usdt"], 7.0)
|
||||||
|
|
||||||
|
def test_merge_perp_options_balances(self):
|
||||||
|
out = merge_perp_options_balances(
|
||||||
|
100,
|
||||||
|
50,
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"enabled": True,
|
||||||
|
"balances": {"funding_usdc": 8, "trading_usdc": 4},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(out["funding_usdt"], 108.0)
|
||||||
|
self.assertEqual(out["trading_usdt"], 54.0)
|
||||||
|
self.assertEqual(out["total_usdt"], 162.0)
|
||||||
|
|
||||||
|
def test_merge_board_row_balances(self):
|
||||||
|
row = {
|
||||||
|
"account_ok": True,
|
||||||
|
"funding_usdt": 20,
|
||||||
|
"trading_usdt": 30,
|
||||||
|
"capabilities": ["options"],
|
||||||
|
"options": {
|
||||||
|
"ok": True,
|
||||||
|
"enabled": True,
|
||||||
|
"balances": {"funding_usdc": 1, "trading_usdc": 2},
|
||||||
|
"positions": [{"inst_id": "X"}],
|
||||||
|
"upl_total_usdc": 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out = merge_board_row_balances(row)
|
||||||
|
self.assertEqual(out["total_usdt"], 53.0)
|
||||||
|
self.assertEqual(out["options_open_position_count"], 1)
|
||||||
|
self.assertEqual(out["options_float_pnl_u"], 0.5)
|
||||||
Reference in New Issue
Block a user