diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 03693a9..8afae33 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -7400,10 +7400,20 @@ def api_account_snapshot(): extra_blocks=not risk_status.get("can_trade", True), ) available_trading_usdt = get_available_trading_usdt() + unrealized_pnl = None + if exchange_private_api_configured(): + from lib.instance.instance_live_pnl_lib import fetch_unrealized_pnl + + def _binance_positions(): + ensure_markets_loaded() + return exchange.fetch_positions() or [] + + unrealized_pnl = fetch_unrealized_pnl(_binance_positions) return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, "available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None, + "unrealized_pnl": unrealized_pnl, "recommended_capital": recommended_capital, "active_count": position_limit_count, "max_active_positions": MAX_ACTIVE_POSITIONS, diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index a59e041..ee79f3f 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -7215,10 +7215,23 @@ def api_account_snapshot(): extra_blocks=not risk_status.get("can_trade", True), ) available_trading_usdt = get_available_trading_usdt() + unrealized_pnl = None + if exchange_private_api_configured(): + from lib.instance.instance_live_pnl_lib import fetch_unrealized_pnl + + def _gate_positions(): + ensure_markets_loaded() + try: + return exchange.fetch_positions(None, {"settle": "usdt"}) or [] + except Exception: + return exchange.fetch_positions() or [] + + unrealized_pnl = fetch_unrealized_pnl(_gate_positions) return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, "available_trading_usdt": round(available_trading_usdt, 2) if available_trading_usdt is not None else None, + "unrealized_pnl": unrealized_pnl, "recommended_capital": recommended_capital, "active_count": position_limit_count, "max_active_positions": MAX_ACTIVE_POSITIONS, diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 528ffa5..6e2c8b8 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6761,10 +6761,23 @@ def api_account_snapshot(): extra_blocks=not risk_status.get("can_trade", True), ) available_trading_usdt = get_available_trading_usdt() + unrealized_pnl = None + if exchange_private_api_configured(): + from lib.instance.instance_live_pnl_lib import fetch_unrealized_pnl + + def _okx_positions(): + ensure_markets_loaded() + try: + return exchange.fetch_positions(None, {"instType": OKX_POSITION_INST_TYPE}) or [] + except Exception: + return exchange.fetch_positions() or [] + + unrealized_pnl = fetch_unrealized_pnl(_okx_positions) return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, "available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None, + "unrealized_pnl": unrealized_pnl, "recommended_capital": recommended_capital, "active_count": position_limit_count, "max_active_positions": MAX_ACTIVE_POSITIONS, diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 27edf15..cf49c12 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -71,6 +71,20 @@ gap: 8px; } + .instance-toolbar-row { + flex-direction: column; + align-items: stretch; + } + + .instance-toolbar-filter.list-window-bar { + flex-basis: auto; + } + + .stat-strip-inner { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .grid { gap: 10px; } @@ -497,6 +511,95 @@ html[data-theme="light"] .theme-toggle-btn.is-active { margin-top: 6px; } +/* ── 顶栏:状态 + 列表筛选 同一行 ── */ +.instance-toolbar-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 10px 14px; + margin-bottom: 12px; +} + +.instance-toolbar-status { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + flex: 0 1 auto; +} + +.instance-toolbar-filter.list-window-bar { + flex: 1 1 320px; + margin-bottom: 0; +} + +.list-window-label { + color: #cfd3ef; +} + +.list-window-hint { + color: #8892b0; + font-size: 0.75rem; +} + +.list-window-apply { + padding: 6px 12px; +} + +/* ── 单行统计卡 ── */ +.stat-strip { + margin-bottom: 12px; + padding: 10px 14px; +} + +.stat-strip-inner { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + align-items: stretch; + justify-content: space-between; +} + +.stat-strip-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 72px; + flex: 1 1 0; + text-align: center; +} + +.stat-strip-item .label { + font-size: 0.72rem; + color: #8892b0; + margin-bottom: 4px; +} + +.stat-strip-item .value { + font-size: 0.95rem; + font-weight: 600; + color: #e8ecff; + line-height: 1.25; +} + +.stat-strip-item--pnl .value.pnl-pos { + color: #3dd68c; +} + +.stat-strip-item--pnl .value.pnl-neg { + color: #ff6b7a; +} + +html[data-theme="light"] .stat-strip-item .value { + color: #142232; +} + +html[data-theme="light"] .list-window-label { + color: #1a2838; +} + .login-theme-bar { display: flex; justify-content: flex-end; @@ -1924,11 +2027,27 @@ html[data-theme="light"] .journal-detail-img-thumb { .settings-cards-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: minmax(0, 1.45fr) minmax(280px, 1fr); gap: 16px; align-items: start; } +.settings-side-col { + display: flex; + flex-direction: column; + gap: 16px; +} + +.settings-card--risk { + min-width: 0; +} + +@media (max-width: 900px) { + .settings-cards-grid { + grid-template-columns: minmax(0, 1fr); + } +} + .settings-card h2 { margin: 0 0 10px; font-size: 1.05rem; diff --git a/lib/instance/instance_live_pnl_lib.py b/lib/instance/instance_live_pnl_lib.py new file mode 100644 index 0000000..5694482 --- /dev/null +++ b/lib/instance/instance_live_pnl_lib.py @@ -0,0 +1,32 @@ +"""实例页:持仓未实现盈亏(实时盈亏)汇总。""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from lib.hub.hub_monitor_totals_lib import position_unrealized_pnl + + +def _position_contracts(pos: dict[str, Any]) -> float: + try: + return abs(float(pos.get("contracts") or 0)) + except (TypeError, ValueError): + return 0.0 + + +def sum_unrealized_pnl_from_positions(positions: list[dict[str, Any]] | None) -> float: + total = 0.0 + for p in positions or []: + if not isinstance(p, dict): + continue + if _position_contracts(p) <= 1e-12: + continue + total += position_unrealized_pnl(p) + return round(total, 2) + + +def fetch_unrealized_pnl(fetch_positions_fn: Callable[[], list[dict[str, Any]] | None]) -> float | None: + try: + return sum_unrealized_pnl_from_positions(fetch_positions_fn() or []) + except Exception: + return None diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index 96c2de4..069f1eb 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1031,6 +1031,21 @@ function refreshAccountSnapshot(){ const el = document.getElementById("current-capital"); if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`; } + if (typeof data.unrealized_pnl !== "undefined") { + const pnlEl = document.getElementById("realtime-pnl"); + if (pnlEl) { + if (data.unrealized_pnl === null || data.unrealized_pnl === undefined) { + pnlEl.innerText = "—"; + pnlEl.classList.remove("pnl-pos", "pnl-neg"); + } else { + const v = Number(data.unrealized_pnl); + const sign = v > 0 ? "+" : ""; + pnlEl.innerText = `${sign}${v.toFixed(2)}U`; + pnlEl.classList.toggle("pnl-pos", v > 0); + pnlEl.classList.toggle("pnl-neg", v < 0); + } + } + } if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) { latestAvailableUsdt = Number(data.available_trading_usdt); } diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index fd0a5bf..e32b587 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -7,7 +7,7 @@ - +
当前账户状态: @@ -36,26 +36,28 @@ {% endfor %}
- CSV · UTF-8 · v{{ instance_settings.data_export_version }}。 - 交易记录含开仓类型列;复盘单独导出。 -
-手动在资金账户与交易账户之间划转 USDT;自动划转规则见下方说明。
+ {% include 'instance_transfer_panel.html' %} +手动在资金账户与交易账户之间划转 USDT;自动划转规则见下方说明。
- {% include 'instance_transfer_panel.html' %} -