diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index b500b45..ac8aa55 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -99,6 +99,8 @@ OKX_POSITION_INST_TYPE=SWAP EXCHANGE_DISPLAY_NAME=OKX # 企业微信推送里展示的账户备注 # OKX_ACCOUNT_LABEL= +# 顶栏是否显示永续资金账户/交易账户(热更);false 时总资金仅计期权侧 +OKX_SHOW_PERP_FUNDS=true # ============================================================================= # 期权(主账户 API,与永续子账户 OKX_API_* 分离;修改后须重启 PM2) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index ec12361..ab2cb0d 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6587,6 +6587,7 @@ def render_main_page(page="trade", embed_mode=None): minimal_stats_bundle, options_funding_label, profit_loss_ratio_from_trades, + show_perp_funds_enabled, total_funds_usdt, trade_records_summary, ) @@ -6738,6 +6739,7 @@ def render_main_page(page="trade", embed_mode=None): _okx_trade_mode = get_okx_trade_mode() _hedge_mode_on = _okx_trade_mode in ("perp_options", "options_options") + _show_perp_funds = show_perp_funds_enabled(exchange_key="okx") template_ctx = dict( page=page, key=key_list, @@ -6749,8 +6751,8 @@ def render_main_page(page="trade", embed_mode=None): rate=rate, profit_loss_ratio=profit_loss_ratio, total_funds=total_funds_usdt( - funding_usdt, - current_capital, + funding_usdt if _show_perp_funds else None, + current_capital if _show_perp_funds else None, options_trading_usdc, options_funding_usdc, options_funding_usdt, @@ -6826,6 +6828,7 @@ def render_main_page(page="trade", embed_mode=None): options_funding_label=options_funding_label, exchange_display=EXCHANGE_DISPLAY_NAME, options_enabled=OKX_OPTIONS_ENABLED, + show_perp_funds=_show_perp_funds, options_nav_visible=True, okx_trade_mode=_okx_trade_mode, options_open_allowed=_okx_trade_mode == "options", @@ -7025,7 +7028,11 @@ def api_account_snapshot(): active_pnl_rows = conn.execute( "SELECT exchange_symbol, symbol, direction FROM order_monitors WHERE status='active'" ).fetchall() - from lib.instance.instance_embed_context_lib import header_trade_stats_for_window, total_funds_usdt + from lib.instance.instance_embed_context_lib import ( + header_trade_stats_for_window, + show_perp_funds_enabled, + total_funds_usdt, + ) header_trade_stats = header_trade_stats_for_window(conn, _list_window_from_request(), APP_TZ) conn.close() @@ -7076,16 +7083,18 @@ def api_account_snapshot(): unrealized_pnl = merge_unrealized_pnl_components(unrealized_pnl, options_unrealized_pnl) except Exception: options_unrealized_pnl = None + _show_perp_funds = show_perp_funds_enabled(exchange_key="okx") return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, + "show_perp_funds": _show_perp_funds, "options_funding_usdc": options_funding_usdc, "options_funding_usdt": options_funding_usdt, "options_trading_usdc": options_trading_usdc, "options_trading_usdt": options_trading_usdt, "total_funds": total_funds_usdt( - funding_usdt, - current_capital, + funding_usdt if _show_perp_funds else None, + current_capital if _show_perp_funds else None, options_trading_usdc, options_funding_usdc, options_funding_usdt, diff --git a/docs/env配置说明.md b/docs/env配置说明.md index bcb1e26..234f511 100644 --- a/docs/env配置说明.md +++ b/docs/env配置说明.md @@ -68,6 +68,7 @@ Binance / Gate 无期权模块时,第三列最后一格不显示或显示「本 | 持仓模式 | 双向 / 单向净持仓等(按所) | 需重启 | | 仓位查询类型 | 仅 OKX:如 SWAP | 需重启 | | 账户备注 | 企业微信推送中显示的交易所备注 | 保存即生效 | +| 显示永续资金 | 仅 OKX:关闭后顶栏隐藏永续资金账户与交易账户,总资金仅计期权侧 | 保存即生效 | **本卡片不包含**:网页登录账号密码,是否关闭登录校验,中控通信密钥. diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index 3c5eaee..a2f43f0 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -89,6 +89,7 @@ HOT_RELOAD_EXACT = frozenset({ "HEDGE_PLAN_ENABLED", "HEDGE_PLAN_SHOW_PERP_OPTIONS", "HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", + "OKX_SHOW_PERP_FUNDS", "OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", "OKX_OPTIONS_MAX_ACTIVE_POSITIONS", "OKX_TRADE_MODE", @@ -214,7 +215,11 @@ def _field_type(key: str, value: str) -> str: low = (value or "").strip().lower() if low in ("true", "false"): return "bool" - if key.endswith("_ENABLED") or key.startswith("RISK_MOOD_"): + if key.endswith("_ENABLED") or key.startswith("RISK_MOOD_") or key in ( + "OKX_SHOW_PERP_FUNDS", + "HEDGE_PLAN_SHOW_PERP_OPTIONS", + "HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", + ): return "bool" try: if "." in low: diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index ad358b0..8e1fc67 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -27,6 +27,11 @@ _EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = { ("OKX_POS_MODE", "持仓模式", ""), ("OKX_POSITION_INST_TYPE", "仓位查询类型", "如 SWAP"), ("OKX_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"), + ( + "OKX_SHOW_PERP_FUNDS", + "显示永续资金", + "默认开启;关闭后顶栏隐藏永续资金账户与交易账户,总资金仅计期权侧", + ), ], "binance": [ ("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"), @@ -236,6 +241,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = { "TRANSFER_CCY": "USDT", "HEDGE_PLAN_SHOW_PERP_OPTIONS": "true", "HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true", + "OKX_SHOW_PERP_FUNDS": "true", "OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED": "true", "OKX_OPTIONS_MAX_ACTIVE_POSITIONS": "0", "OKX_TRADE_MODE": "options", diff --git a/lib/instance/instance_embed_context_lib.py b/lib/instance/instance_embed_context_lib.py index 22599e5..ca6b78e 100644 --- a/lib/instance/instance_embed_context_lib.py +++ b/lib/instance/instance_embed_context_lib.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from dataclasses import dataclass from typing import Any @@ -10,6 +11,20 @@ EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", _WIN_EPS = 1e-9 +def env_truthy(raw: str | None, default: bool = False) -> bool: + if raw is None or str(raw).strip() == "": + return default + return str(raw).strip().lower() in ("1", "true", "yes", "on") + + +def show_perp_funds_enabled(*, exchange_key: str | None = None) -> bool: + """OKX:是否在顶栏显示永续资金账户/交易账户.其他所恒为 True.""" + ex = (exchange_key or "").strip().lower() + if ex and ex != "okx": + return True + return env_truthy(os.getenv("OKX_SHOW_PERP_FUNDS"), default=True) + + @dataclass(frozen=True) class EmbedRenderPlan: exchange_capitals: bool diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index 7c2edba..af052d7 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1149,8 +1149,23 @@ function setFundsFieldText(field, text){ el.innerText = text; }); } +function applyPerpFundsVisibility(show){ + const on = show !== false; + document.querySelectorAll("[data-perp-funds='1']").forEach((el) => { + el.style.display = on ? "" : "none"; + }); +} function accountSnapshotFundingMissing(data){ if(!data || typeof data !== "object") return true; + if(data.show_perp_funds === false){ + const hasTotal = data.total_funds != null && data.total_funds !== ""; + const hasOpt = + data.options_funding_usdc != null || + data.options_funding_usdt != null || + data.options_trading_usdc != null || + data.options_trading_usdt != null; + return !hasTotal && !hasOpt; + } const hasFunding = data.funding_usdt != null && data.funding_usdt !== ""; const hasTotal = data.total_funds != null && data.total_funds !== ""; const hasTrading = data.current_capital != null && data.current_capital !== ""; @@ -1159,6 +1174,9 @@ function accountSnapshotFundingMissing(data){ let accountSnapshotRetryCount = 0; function applyAccountSnapshot(data){ if(!data || typeof data !== "object") return; + if(typeof data.show_perp_funds !== "undefined"){ + applyPerpFundsVisibility(data.show_perp_funds); + } if(data.funding_usdt != null && data.funding_usdt !== ""){ setFundsFieldText("total-capital", `${Number(data.funding_usdt).toFixed(2)}U`); } diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 79bdbb1..8e621e1 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -1626,8 +1626,23 @@ function setFundsFieldText(field, text){ el.innerText = text; }); } +function applyPerpFundsVisibility(show){ + const on = show !== false; + document.querySelectorAll("[data-perp-funds='1']").forEach((el) => { + el.style.display = on ? "" : "none"; + }); +} function accountSnapshotFundingMissing(data){ if(!data || typeof data !== "object") return true; + if(data.show_perp_funds === false){ + const hasTotal = data.total_funds != null && data.total_funds !== ""; + const hasOpt = + data.options_funding_usdc != null || + data.options_funding_usdt != null || + data.options_trading_usdc != null || + data.options_trading_usdt != null; + return !hasTotal && !hasOpt; + } const hasFunding = data.funding_usdt != null && data.funding_usdt !== ""; const hasTotal = data.total_funds != null && data.total_funds !== ""; const hasTrading = data.current_capital != null && data.current_capital !== ""; @@ -1636,6 +1651,9 @@ function accountSnapshotFundingMissing(data){ let accountSnapshotRetryCount = 0; function applyAccountSnapshot(data){ if(!data || typeof data !== "object") return; + if(typeof data.show_perp_funds !== "undefined"){ + applyPerpFundsVisibility(data.show_perp_funds); + } if(data.funding_usdt != null && data.funding_usdt !== ""){ setFundsFieldText("total-capital", `${Number(data.funding_usdt).toFixed(2)}U`); } diff --git a/lib/instance/templates/instance_header_panel.html b/lib/instance/templates/instance_header_panel.html index a69e2ab..f2cb7a4 100644 --- a/lib/instance/templates/instance_header_panel.html +++ b/lib/instance/templates/instance_header_panel.html @@ -38,11 +38,11 @@ {% include 'instance_header_stats.html' %}
- + - + diff --git a/lib/instance/templates/instance_header_stats.html b/lib/instance/templates/instance_header_stats.html index 86b3416..a553391 100644 --- a/lib/instance/templates/instance_header_stats.html +++ b/lib/instance/templates/instance_header_stats.html @@ -24,11 +24,11 @@
总资金
{% if total_funds is not none %}{{ funds_fmt(total_funds) }}U{% else %}—{% endif %}
-
+ -
+ diff --git a/tests/test_instance_display_env_settings.py b/tests/test_instance_display_env_settings.py index 97c7dbc..478e5d1 100644 --- a/tests/test_instance_display_env_settings.py +++ b/tests/test_instance_display_env_settings.py @@ -108,6 +108,10 @@ class TestEnvSchema(unittest.TestCase): self.assertEqual(by_key["KEY_AUTO_ORDER_ENABLED"]["label"], "关键位自动单") self.assertEqual(by_key["KEY_AUTO_ORDER_ENABLED"]["type"], "bool") self.assertIn("KEY_AUTO_MIN_PLANNED_RR", by_key) + self.assertIn("OKX_SHOW_PERP_FUNDS", by_key) + self.assertEqual(by_key["OKX_SHOW_PERP_FUNDS"]["label"], "显示永续资金") + self.assertEqual(by_key["OKX_SHOW_PERP_FUNDS"]["type"], "bool") + self.assertTrue(by_key["OKX_SHOW_PERP_FUNDS"].get("hot_reload")) groups_v = [{"title": "t", "fields": [by_key["TRADE_DIRECTION"]]}] clean, errors = validate_env_updates(groups_v, {"TRADE_DIRECTION": "long_only"}) @@ -117,5 +121,22 @@ class TestEnvSchema(unittest.TestCase): self.assertTrue(bad) +class TestShowPerpFunds(unittest.TestCase): + def test_okx_default_on(self): + from lib.instance.instance_embed_context_lib import show_perp_funds_enabled + + old = os.environ.pop("OKX_SHOW_PERP_FUNDS", None) + try: + self.assertTrue(show_perp_funds_enabled(exchange_key="okx")) + os.environ["OKX_SHOW_PERP_FUNDS"] = "false" + self.assertFalse(show_perp_funds_enabled(exchange_key="okx")) + self.assertTrue(show_perp_funds_enabled(exchange_key="gate")) + finally: + if old is None: + os.environ.pop("OKX_SHOW_PERP_FUNDS", None) + else: + os.environ["OKX_SHOW_PERP_FUNDS"] = old + + if __name__ == "__main__": unittest.main()