diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index d1b33a7..b034de4 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6626,22 +6626,36 @@ def render_main_page(page="trade", embed_mode=None): options_funding_usdc = None options_funding_usdt = None options_trading_usdt = None + options_funding_eth = None + options_trading_eth = None + options_margin_mode = "usdc" + options_underly = "ETH" if ( OKX_OPTIONS_ENABLED and exchange_options.apiKey and embed_mode != "fragment" ): try: - from lib.exchange.okx_options_lib import options_header_balances + from lib.exchange.okx_options_lib import options_header_balance_pack - options_trading_usdc, options_funding_usdc, options_funding_usdt, options_trading_usdt = options_header_balances( - exchange_options - ) + _op = options_header_balance_pack(exchange_options) + options_trading_usdc = _op.get("trading_usdc") + options_funding_usdc = _op.get("funding_usdc") + options_funding_usdt = _op.get("funding_usdt") + options_trading_usdt = _op.get("trading_usdt") + options_funding_eth = _op.get("funding_coin") + options_trading_eth = _op.get("trading_coin") + options_margin_mode = _op.get("options_margin_mode") or "usdc" + options_underly = _op.get("options_underly") or "ETH" except Exception: options_trading_usdc = None options_funding_usdc = None options_funding_usdt = None options_trading_usdt = None + options_funding_eth = None + options_trading_eth = None + options_margin_mode = "usdc" + options_underly = "ETH" recommended_capital = get_recommended_capital(current_capital) key_list = ( conn.execute("SELECT * FROM key_monitors").fetchall() if plan.key_list else [] @@ -6793,6 +6807,10 @@ def render_main_page(page="trade", embed_mode=None): options_funding_usdt=options_funding_usdt, options_trading_usdc=options_trading_usdc, options_trading_usdt=options_trading_usdt, + options_funding_eth=options_funding_eth, + options_trading_eth=options_trading_eth, + options_margin_mode=options_margin_mode, + options_underly=options_underly, trading_day=trading_day, daily_start_capital=DAILY_START_CAPITAL, current_capital=current_capital, @@ -7050,19 +7068,32 @@ def api_account_snapshot(): options_funding_usdc = None options_funding_usdt = None options_trading_usdt = None + options_funding_eth = None + options_trading_eth = None + options_margin_mode = "usdc" + options_underly = "ETH" if OKX_OPTIONS_ENABLED and exchange_options.apiKey: try: - from lib.exchange.okx_options_lib import options_header_balances + from lib.exchange.okx_options_lib import options_header_balance_pack - options_trading_usdc, options_funding_usdc, options_funding_usdt, options_trading_usdt = options_header_balances( - exchange_options, - force=force_refresh, - ) + _op = options_header_balance_pack(exchange_options, force=force_refresh) + options_trading_usdc = _op.get("trading_usdc") + options_funding_usdc = _op.get("funding_usdc") + options_funding_usdt = _op.get("funding_usdt") + options_trading_usdt = _op.get("trading_usdt") + options_funding_eth = _op.get("funding_coin") + options_trading_eth = _op.get("trading_coin") + options_margin_mode = _op.get("options_margin_mode") or "usdc" + options_underly = _op.get("options_underly") or "ETH" except Exception: options_trading_usdc = None options_funding_usdc = None options_funding_usdt = None options_trading_usdt = None + options_funding_eth = None + options_trading_eth = None + options_margin_mode = "usdc" + options_underly = "ETH" recommended_capital = get_recommended_capital(current_capital) from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors @@ -7145,6 +7176,10 @@ def api_account_snapshot(): "options_funding_usdt": options_funding_usdt, "options_trading_usdc": options_trading_usdc, "options_trading_usdt": options_trading_usdt, + "options_funding_eth": options_funding_eth, + "options_trading_eth": options_trading_eth, + "options_margin_mode": options_margin_mode, + "options_underly": options_underly, "total_funds": total_funds_usdt( funding_usdt if _show_perp_funds else None, current_capital if _show_perp_funds else None, diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index 9d4451d..b62525a 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -166,6 +166,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = { ("perp_options", "永期对冲"), ("options_options", "期期对冲"), ), + "OKX_OPTIONS_MARGIN_MODE": ( + ("usdc", "USDC(USDⓈ权利金)"), + ("coin", "币本位(USDT买币桥)"), + ), "HEDGE_PLAN_OPTION_PRIMARY": ( ("true", "以期权为主"), ("false", "保险模式"), diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index afba271..ff09200 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -146,7 +146,7 @@ _OPTIONS_SECTION: dict[str, Any] = { ( "OKX_OPTIONS_MARGIN_MODE", "单笔期权本位", - "usdc=USDⓈ权利金(现状);coin=币本位+USDT买币桥。有持仓/半成品桥时勿切换;改后需重启", + "usdc=USDⓈ权利金;coin=币本位+USDT买币桥。有持仓/半成品桥时勿切换;改后需重启", ), ( "OKX_OPTIONS_TRADE_BUDGET_USDC", diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 8ad7ff7..ea33ba0 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -509,8 +509,8 @@ def fetch_account_balances_by_type( ex: ccxt.okx, account_type: str, ) -> tuple[dict[str, float | None], dict[str, float | None]]: - out: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None} - avail: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None} + out: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None, "ETH": None, "BTC": None} + avail: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None, "ETH": None, "BTC": None} try: bal = ex.fetch_balance(params={"type": account_type}) for c in out: @@ -525,8 +525,8 @@ def fetch_funding_balances_via_asset_api( ex: ccxt.okx, ) -> tuple[dict[str, float | None], dict[str, float | None]]: """OKX 资金账户余额(GET /api/v5/asset/balances),比 ccxt fetch_balance 更准确.""" - out: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None} - avail: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None} + out: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None, "ETH": None, "BTC": None} + avail: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None, "ETH": None, "BTC": None} try: resp = ex.private_get_asset_balances({}) for row in (resp or {}).get("data") or []: @@ -620,13 +620,21 @@ def fetch_options_balances( "funding_usdt": funding.get("USDT"), "funding_usdc": funding.get("USDC"), "funding_usdg": funding.get("USDG"), + "funding_eth": funding.get("ETH"), + "funding_btc": funding.get("BTC"), "funding_usdt_avail": funding_avail.get("USDT"), "funding_usdc_avail": funding_avail.get("USDC"), + "funding_eth_avail": funding_avail.get("ETH"), + "funding_btc_avail": funding_avail.get("BTC"), "trading_usdt": trading.get("USDT"), "trading_usdc": trading.get("USDC"), "trading_usdg": trading.get("USDG"), + "trading_eth": trading.get("ETH"), + "trading_btc": trading.get("BTC"), "trading_usdt_avail": trading_avail.get("USDT"), "trading_usdc_avail": trading_avail.get("USDC"), + "trading_eth_avail": trading_avail.get("ETH"), + "trading_btc_avail": trading_avail.get("BTC"), } _OPTIONS_BALANCE_CACHE["updated_at"] = now _OPTIONS_BALANCE_CACHE["data"] = result @@ -642,22 +650,63 @@ def options_header_balances( 返回:(trading_usdc, funding_usdc, funding_usdt, trading_usdt) """ + pack = options_header_balance_pack(ex, force=force) + return ( + pack.get("trading_usdc"), + pack.get("funding_usdc"), + pack.get("funding_usdt"), + pack.get("trading_usdt"), + ) + + +def options_header_balance_pack( + ex: ccxt.okx, + *, + force: bool = False, +) -> dict[str, Any]: + """顶栏/快照用期权资金包(含币本位 ETH/BTC).""" + import os + bal = fetch_options_balances(ex, force=force) - def _round(v: Any) -> float | None: + def _round(v: Any, nd: int = 2) -> float | None: if v is None: return None try: - return round(float(v), 2) + return round(float(v), nd) except (TypeError, ValueError): return None - return ( - _round(bal.get("trading_usdc")), - _round(bal.get("funding_usdc")), - _round(bal.get("funding_usdt")), - _round(bal.get("trading_usdt")), - ) + def _round_coin(v: Any) -> float | None: + if v is None: + return None + try: + return round(float(v), 8) + except (TypeError, ValueError): + return None + + try: + from lib.options.options_margin_mode_lib import normalize_options_margin_mode + + margin_mode = normalize_options_margin_mode() + except Exception: + margin_mode = "usdc" + underly = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH" + coin_key = "btc" if underly == "BTC" else "eth" + return { + "trading_usdc": _round(bal.get("trading_usdc")), + "funding_usdc": _round(bal.get("funding_usdc")), + "funding_usdt": _round(bal.get("funding_usdt")), + "trading_usdt": _round(bal.get("trading_usdt")), + "funding_eth": _round_coin(bal.get("funding_eth")), + "trading_eth": _round_coin(bal.get("trading_eth")), + "funding_btc": _round_coin(bal.get("funding_btc")), + "trading_btc": _round_coin(bal.get("trading_btc")), + "options_margin_mode": margin_mode, + "options_underly": underly, + "funding_coin": _round_coin(bal.get(f"funding_{coin_key}")), + "trading_coin": _round_coin(bal.get(f"trading_{coin_key}")), + } def fetch_index_price(ex: ccxt.okx, uly: str) -> float | None: diff --git a/lib/instance/instance_embed_context_lib.py b/lib/instance/instance_embed_context_lib.py index a9c5507..22704da 100644 --- a/lib/instance/instance_embed_context_lib.py +++ b/lib/instance/instance_embed_context_lib.py @@ -103,9 +103,38 @@ def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float def options_funding_label( funding_usdc: float | None, funding_usdt: float | None = None, + funding_eth: float | None = None, + margin_mode: str | None = None, + underly: str = "ETH", ) -> str: - """期权侧顶栏仅展示 USDC(USDT 归永续资金/交易账户).funding_usdt 参数保留兼容,忽略.""" - _ = funding_usdt + """期权侧顶栏文案. + + USDC 模式:仅 USDC. + 币本位:USDT + 标的币(ETH/BTC). + """ + try: + from lib.options.options_margin_mode_lib import normalize_options_margin_mode + + mode = normalize_options_margin_mode(margin_mode) + except Exception: + mode = str(margin_mode or "usdc").strip().lower() or "usdc" + if mode == "coin": + parts: list[str] = [] + if funding_usdt is not None: + try: + parts.append(f"{float(funding_usdt):.2f} USDT") + except (TypeError, ValueError): + pass + coin = funding_eth + ccy = (underly or "ETH").strip().upper() or "ETH" + if coin is not None: + try: + n = float(coin) + txt = f"{n:.6f}".rstrip("0").rstrip(".") + parts.append(f"{txt or '0'} {ccy}") + except (TypeError, ValueError): + pass + return " / ".join(parts) if parts else "—" if funding_usdc is None: return "—" try: diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index 96cb39f..a289fc5 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1136,8 +1136,23 @@ function paintRealtimePnlFromSnapshot(data){ } } -function formatOptionsFundingLabel(usdc, usdt) { - // 期权侧顶栏仅 USDC;usdt 参数忽略(USDT 在永续资金/交易账户) +function formatOptionsFundingLabel(usdc, usdt, eth, marginMode, underly) { + const mode = String(marginMode || "usdc").toLowerCase(); + if (mode === "coin") { + const parts = []; + if (usdt !== null && usdt !== undefined && usdt !== "") { + const n = Number(usdt); + if (!Number.isNaN(n)) parts.push(`${n.toFixed(2)} USDT`); + } + if (eth !== null && eth !== undefined && eth !== "") { + const n = Number(eth); + if (!Number.isNaN(n)) { + const txt = String(n.toFixed(6)).replace(/\.?0+$/, ""); + parts.push(`${txt || "0"} ${String(underly || "ETH").toUpperCase()}`); + } + } + return parts.length ? parts.join(" / ") : "—"; + } if (usdc === null || usdc === undefined || usdc === "") return "—"; const n = Number(usdc); if (Number.isNaN(n)) return "—"; @@ -1187,12 +1202,24 @@ function applyAccountSnapshot(data){ if(data.current_capital != null && data.current_capital !== "" && !Number.isNaN(Number(data.current_capital))){ setFundsFieldText("current-capital", `${Number(data.current_capital).toFixed(2)}U`); } - if(data.options_funding_usdc != null || data.options_funding_usdt != null){ - const optFunding = formatOptionsFundingLabel(data.options_funding_usdc, data.options_funding_usdt); + if(data.options_funding_usdc != null || data.options_funding_usdt != null || data.options_funding_eth != null){ + const optFunding = formatOptionsFundingLabel( + data.options_funding_usdc, + data.options_funding_usdt, + data.options_funding_eth, + data.options_margin_mode, + data.options_underly + ); setFundsFieldText("options-funding-usdc", optFunding); } - if(data.options_trading_usdc != null || data.options_trading_usdt != null){ - const optTrading = formatOptionsFundingLabel(data.options_trading_usdc, data.options_trading_usdt); + if(data.options_trading_usdc != null || data.options_trading_usdt != null || data.options_trading_eth != null){ + const optTrading = formatOptionsFundingLabel( + data.options_trading_usdc, + data.options_trading_usdt, + data.options_trading_eth, + data.options_margin_mode, + data.options_underly + ); setFundsFieldText("options-trading-usdc", optTrading); } if(typeof data.unrealized_pnl !== "undefined"){ diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 3354750..74564c1 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -1617,8 +1617,23 @@ function paintRealtimePnlFromSnapshot(data){ } } -function formatOptionsFundingLabel(usdc, usdt) { - // 期权侧顶栏仅 USDC;usdt 参数忽略(USDT 在永续资金/交易账户) +function formatOptionsFundingLabel(usdc, usdt, eth, marginMode, underly) { + const mode = String(marginMode || "usdc").toLowerCase(); + if (mode === "coin") { + const parts = []; + if (usdt != null && usdt !== "") { + const n = Number(usdt); + if (!Number.isNaN(n)) parts.push(`${n.toFixed(2)} USDT`); + } + if (eth != null && eth !== "") { + const n = Number(eth); + if (!Number.isNaN(n)) { + const txt = String(n.toFixed(6)).replace(/\.?0+$/, ""); + parts.push(`${txt || "0"} ${String(underly || "ETH").toUpperCase()}`); + } + } + return parts.length ? parts.join(" / ") : "—"; + } if(usdc == null || usdc === "") return "—"; const n = Number(usdc); if(Number.isNaN(n)) return "—"; @@ -1668,12 +1683,24 @@ function applyAccountSnapshot(data){ if(data.current_capital != null && data.current_capital !== "" && !Number.isNaN(Number(data.current_capital))){ setFundsFieldText("current-capital", `${Number(data.current_capital).toFixed(2)}U`); } - if(data.options_funding_usdc != null || data.options_funding_usdt != null){ - const optFunding = formatOptionsFundingLabel(data.options_funding_usdc, data.options_funding_usdt); + if(data.options_funding_usdc != null || data.options_funding_usdt != null || data.options_funding_eth != null){ + const optFunding = formatOptionsFundingLabel( + data.options_funding_usdc, + data.options_funding_usdt, + data.options_funding_eth, + data.options_margin_mode, + data.options_underly + ); setFundsFieldText("options-funding-usdc", optFunding); } - if(data.options_trading_usdc != null || data.options_trading_usdt != null){ - const optTrading = formatOptionsFundingLabel(data.options_trading_usdc, data.options_trading_usdt); + if(data.options_trading_usdc != null || data.options_trading_usdt != null || data.options_trading_eth != null){ + const optTrading = formatOptionsFundingLabel( + data.options_trading_usdc, + data.options_trading_usdt, + data.options_trading_eth, + data.options_margin_mode, + data.options_underly + ); setFundsFieldText("options-trading-usdc", optTrading); } if(typeof data.unrealized_pnl !== "undefined"){ diff --git a/lib/instance/templates/instance_header_stats.html b/lib/instance/templates/instance_header_stats.html index 6c5083b..15d18d7 100644 --- a/lib/instance/templates/instance_header_stats.html +++ b/lib/instance/templates/instance_header_stats.html @@ -35,11 +35,11 @@ {% if options_enabled %}