From e7aa724a3bf292630ede56ea92a986599ee65237 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 14 Aug 2026 18:35:08 +0800 Subject: [PATCH] Show okx_sim/okx_live and gate live API fields by matching mode. In sim, hide LIVE/API keys and expose simulated fund settings in env UI. Co-authored-by: Cursor --- app.py | 17 ++++- lib/common/static/instance_settings_prefs.js | 57 ++++++++++++--- lib/env/env_schema.py | 3 + lib/env/env_ui_manifest.py | 73 ++++++++++++++++++- .../templates/embed_boot_scripts.html | 3 + lib/instance/templates/embed_shell.html | 2 +- lib/instance/templates/index.html | 5 +- .../templates/instance_header_stats.html | 2 +- lib/sim/mode_lib.py | 8 ++ 9 files changed, 150 insertions(+), 20 deletions(-) diff --git a/app.py b/app.py index 65ad1b5..440461c 100644 --- a/app.py +++ b/app.py @@ -5046,11 +5046,18 @@ def render_main_page(page="options", embed_mode=None): options_funding_usdt = None options_trading_usdt = None _sim_mode_for_header = False + _exchange_display_for_header = EXCHANGE_DISPLAY_NAME try: + from lib.sim.mode_lib import exchange_mode_label as _exchange_mode_label from lib.sim.mode_lib import is_sim_mode as _is_sim_mode_fn + _sim_mode_for_header = bool(_is_sim_mode_fn(get_db)) + _exchange_display_for_header = _exchange_mode_label( + is_sim=_sim_mode_for_header, exchange_key="okx" + ) except Exception: _sim_mode_for_header = False + _exchange_display_for_header = EXCHANGE_DISPLAY_NAME if ( OKX_OPTIONS_ENABLED and embed_mode != "fragment" @@ -5258,7 +5265,7 @@ def render_main_page(page="options", embed_mode=None): key_rule_ctx=key_rule_ctx, funds_fmt=format_funds_u, options_funding_label=options_funding_label, - exchange_display=EXCHANGE_DISPLAY_NAME, + exchange_display=_exchange_display_for_header, options_enabled=OKX_OPTIONS_ENABLED, trading_mode=("sim" if _sim_mode_for_header else "live"), is_sim_mode=_sim_mode_for_header, @@ -5532,10 +5539,18 @@ def api_account_snapshot(): except Exception: options_unrealized_pnl = None _show_perp_funds = show_perp_funds_enabled(exchange_key="okx") + try: + from lib.sim.mode_lib import exchange_mode_label as _ex_mode_label + from lib.sim.mode_lib import is_sim_mode as _is_sim + + _snapshot_mode_label = _ex_mode_label(is_sim=_is_sim(get_db), exchange_key="okx") + except Exception: + _snapshot_mode_label = EXCHANGE_DISPLAY_NAME return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, "show_perp_funds": _show_perp_funds, + "exchange_mode_label": _snapshot_mode_label, "options_funding_usdc": options_funding_usdc, "options_funding_usdt": options_funding_usdt, "options_trading_usdc": options_trading_usdc, diff --git a/lib/common/static/instance_settings_prefs.js b/lib/common/static/instance_settings_prefs.js index 4565900..1d93e73 100644 --- a/lib/common/static/instance_settings_prefs.js +++ b/lib/common/static/instance_settings_prefs.js @@ -386,28 +386,61 @@ } function bindTradeModeAutoRefresh(body) { - const modeSel = body.querySelector('.env-field-input[data-env-key="OKX_TRADE_MODE"]'); - if (!modeSel || modeSel.dataset.modeRefreshBound === "1") return; - modeSel.dataset.modeRefreshBound = "1"; + bindEnvSelectAutoRefresh( + body, + "OKX_TRADE_MODE", + "切换交易模式并刷新配置…", + "交易模式已切换为当前选项,配置区已刷新", + "modeRefreshBound", + true + ); + bindEnvSelectAutoRefresh( + body, + "SIM_DEFAULT_MODE", + "切换撮合模式并刷新配置…", + "撮合模式已切换,配置区已刷新(sim 隐藏 API / live 显示实盘项)", + "simModeRefreshBound", + false + ); + } + + function bindEnvSelectAutoRefresh(body, key, pendingMsg, okMsg, flag, jumpModeSection) { + const modeSel = body && body.querySelector('.env-field-input[data-env-key="' + key + '"]'); + if (!modeSel || modeSel.dataset[flag] === "1") return; + modeSel.dataset[flag] = "1"; modeSel.addEventListener("change", async () => { const status = document.getElementById("env-config-status"); const nextMode = modeSel.value; - setStatus(status, "切换交易模式并刷新配置…"); + setStatus(status, pendingMsg); try { + const payload = { values: {} }; + payload.values[key] = nextMode; await fetchJson("/api/settings/env", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ values: { OKX_TRADE_MODE: nextMode } }), + body: JSON.stringify(payload), }); await loadEnvConfig(true); - const page = envConfigRoot() || document.querySelector(".env-config-page"); - const newBody = page && page.querySelector("#env-config-body"); - const idx = newBody && newBody.dataset.envModeSectionIdx; - if (idx != null) { - const radio = document.getElementById("env-sec-" + idx); - if (radio) radio.checked = true; + if (jumpModeSection) { + const page = envConfigRoot() || document.querySelector(".env-config-page"); + const newBody = page && page.querySelector("#env-config-body"); + const idx = newBody && newBody.dataset.envModeSectionIdx; + if (idx != null) { + const radio = document.getElementById("env-sec-" + idx); + if (radio) radio.checked = true; + } + } + setStatus(status, okMsg); + if (typeof global.refreshAccountSnapshot === "function") { + global.refreshAccountSnapshot({ force: true, silent: true }); + } + // 顶栏交易所名:okx_sim / okx_live + const label = nextMode === "sim" ? "okx_sim" : nextMode === "live" ? "okx_live" : ""; + if (label) { + document.querySelectorAll('[data-funds-field="exchange-mode-label"]').forEach((el) => { + el.textContent = label; + }); } - setStatus(status, "交易模式已切换为当前选项,配置区已刷新"); } catch (e) { setStatus(status, e.message || "切换失败", true); } diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index 88f0150..bd4e47e 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -100,6 +100,9 @@ HOT_RELOAD_EXACT = frozenset({ "OKX_OPTIONS_BUDGET_BUFFER", "OKX_TRADE_MODE", "SIM_DEFAULT_MODE", + "SIM_INITIAL_EQUITY_USDT", + "SIM_INITIAL_USDC", + "SIM_FEE_RATE", "MAX_ACTIVE_HEDGE_PLANS", "HEDGE_PLAN_LIVE_ORDER", "HEDGE_PLAN_OPTION_PRIMARY", diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index 37555ca..7cac240 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -17,12 +17,42 @@ from lib.env.env_schema import ( ) # 各所「交易所与实盘」字段(顺序即页面顺序) +_OKX_LIVE_ONLY_KEYS = frozenset( + { + "LIVE_TRADING_ENABLED", + "OKX_API_KEY", + "OKX_API_SECRET", + "OKX_API_PASSPHRASE", + } +) + +_SIM_FUNDS_SECTION: dict[str, Any] = { + "title": "模拟资金", + "fields": [ + ( + "SIM_INITIAL_EQUITY_USDT", + "初始权益 USDT", + "重置模拟钱包时写入资金账户 USDT;改完需在系统设置「模拟资金」点重置才生效", + ), + ( + "SIM_INITIAL_USDC", + "初始 USDC", + "重置时写入期权侧 USDC;改完需重置才生效", + ), + ( + "SIM_FEE_RATE", + "模拟手续费率", + "如 0.0005=万五;撮合立即按此费率扣费", + ), + ], +} + _EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = { "okx": [ ( "SIM_DEFAULT_MODE", "撮合模式", - "sim=本地模拟资金(公开行情撮合,不下真实单); live=实盘账户.保存后立即切换", + "sim=本地模拟资金; live=实盘.保存后立即切换;sim 下隐藏 API/实盘开关", ), ("LIVE_TRADING_ENABLED", "开启实盘下单", "仅 live 模式下生效;关闭时即使 live 也不向交易所发单"), ("OKX_API_KEY", "API Key", "账户 API(永续+期权共用)"), @@ -372,15 +402,41 @@ def _hedge_fields_for_mode(mode: str) -> list[tuple[str, str, str]]: return [] +def _trading_mode_for_env_ui() -> str: + try: + from lib.sim.mode_lib import peek_persisted_trading_mode, default_trading_mode + + return peek_persisted_trading_mode() or default_trading_mode() + except Exception: + return "sim" + + +def _okx_exchange_fields_for_trading_mode(trading_mode: str) -> list[tuple[str, str, str]]: + fields = list(_EXCHANGE_LIVE_FIELDS["okx"]) + tm = (trading_mode or "").strip().lower() + if tm == "sim": + return [f for f in fields if f[0] not in _OKX_LIVE_ONLY_KEYS] + return fields + + def ui_sections_for_exchange( exchange_key: str, *, mode: str | None = None, + trading_mode: str | None = None, ) -> list[dict[str, Any]]: ex = (exchange_key or "").strip().lower() sections: list[dict[str, Any]] = [] - live_fields = _EXCHANGE_LIVE_FIELDS.get(ex, _EXCHANGE_LIVE_FIELDS["okx"]) + tm = (trading_mode or "").strip().lower() + if not tm: + tm = _trading_mode_for_env_ui() if ex == "okx" else "live" + if ex == "okx": + live_fields = _okx_exchange_fields_for_trading_mode(tm) + else: + live_fields = _EXCHANGE_LIVE_FIELDS.get(ex, _EXCHANGE_LIVE_FIELDS["okx"]) sections.append({"title": "交易所与实盘", "fields": live_fields}) + if ex == "okx" and tm == "sim": + sections.append(_SIM_FUNDS_SECTION) sections.extend(_SHARED_SECTIONS) if ex in _MODE_SECTION.get("exchanges", frozenset()): from lib.hedge_plan.okx_trade_mode_lib import normalize_okx_trade_mode @@ -406,7 +462,11 @@ def ui_allowed_keys(exchange_key: str) -> frozenset[str]: ex = (exchange_key or "").strip().lower() if ex == "okx": keys.add("OKX_TRADE_MODE") - # 允许写入遗留键,避免旧自动化/手改失败;页面不再展示 + keys.add("SIM_DEFAULT_MODE") + # 切模式后同请求可能带上对侧字段,始终放行 + keys.update(_OKX_LIVE_ONLY_KEYS) + for item in _SIM_FUNDS_SECTION["fields"]: + keys.add(item[0]) for item in _HEDGE_PLAN_SECTION["fields"]: keys.add(item[0]) for item in _OPTIONS_SECTION["fields"]: @@ -422,9 +482,14 @@ def build_env_ui_payload( schema = _schema_field_map(example_path) env_lines = read_env_lines(env_path) values = env_get_all(env_lines) + trading_mode = "" + if (exchange_key or "").strip().lower() == "okx": + trading_mode = _effective_env_value("SIM_DEFAULT_MODE", values, "sim") or _trading_mode_for_env_ui() groups: list[dict[str, Any]] = [] for sec in ui_sections_for_exchange( - exchange_key, mode=values.get("OKX_TRADE_MODE") or "" + exchange_key, + mode=values.get("OKX_TRADE_MODE") or "", + trading_mode=trading_mode, ): fields = [ _build_field(key, label, note, schema, values) diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index ac79c2e..d38e3b5 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1178,6 +1178,9 @@ function applyAccountSnapshot(data){ if(typeof data.show_perp_funds !== "undefined"){ applyPerpFundsVisibility(data.show_perp_funds); } + if(data.exchange_mode_label){ + setFundsFieldText("exchange-mode-label", data.exchange_mode_label); + } if(data.funding_usdt != null && data.funding_usdt !== ""){ setFundsFieldText("total-capital", `${Number(data.funding_usdt).toFixed(2)}U`); } diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index ed268c1..b1f4527 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -144,7 +144,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj - + diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 7cca29a..f353bea 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -1444,6 +1444,9 @@ function applyAccountSnapshot(data){ if(typeof data.show_perp_funds !== "undefined"){ applyPerpFundsVisibility(data.show_perp_funds); } + if(data.exchange_mode_label){ + setFundsFieldText("exchange-mode-label", data.exchange_mode_label); + } if(data.funding_usdt != null && data.funding_usdt !== ""){ setFundsFieldText("total-capital", `${Number(data.funding_usdt).toFixed(2)}U`); } @@ -1829,6 +1832,6 @@ document.addEventListener("DOMContentLoaded", function () { }); {% endif %} - + \ No newline at end of file diff --git a/lib/instance/templates/instance_header_stats.html b/lib/instance/templates/instance_header_stats.html index 642255b..0492ed1 100644 --- a/lib/instance/templates/instance_header_stats.html +++ b/lib/instance/templates/instance_header_stats.html @@ -2,7 +2,7 @@
交易所
-
{{ exchange_display }}{% if is_sim_mode|default(false) %} 模拟{% endif %}
+
{{ exchange_display }}
交易日
diff --git a/lib/sim/mode_lib.py b/lib/sim/mode_lib.py index 3ac7d8b..73c64bd 100644 --- a/lib/sim/mode_lib.py +++ b/lib/sim/mode_lib.py @@ -78,3 +78,11 @@ def set_trading_mode(get_db: Callable, mode: str) -> str: def is_sim_mode(get_db: Callable) -> bool: return get_trading_mode(get_db) == MODE_SIM + + +def exchange_mode_label(*, is_sim: bool | None = None, get_db: Callable | None = None, exchange_key: str = "okx") -> str: + """顶栏交易所展示: okx_sim / okx_live.""" + ex = (exchange_key or "okx").strip().lower() or "okx" + if is_sim is None and callable(get_db): + is_sim = is_sim_mode(get_db) + return f"{ex}_sim" if bool(is_sim) else f"{ex}_live"