diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 5388233..63a48b5 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6507,13 +6507,16 @@ def render_main_page(page="trade", embed_mode=None): funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS) options_trading_usdc = None + options_funding_usdc = None if OKX_OPTIONS_ENABLED and exchange_options.apiKey: try: - from lib.exchange.okx_options_lib import fetch_options_trading_usdc + from lib.exchange.okx_options_lib import fetch_options_funding_usdc, fetch_options_trading_usdc options_trading_usdc = fetch_options_trading_usdc(exchange_options) + options_funding_usdc = fetch_options_funding_usdc(exchange_options) except Exception: options_trading_usdc = None + options_funding_usdc = None recommended_capital = get_recommended_capital(current_capital) key_list = ( conn.execute("SELECT * FROM key_monitors").fetchall() if plan.key_list else [] @@ -6631,7 +6634,10 @@ def render_main_page(page="trade", embed_mode=None): total=total, rate=rate, profit_loss_ratio=profit_loss_ratio, - total_funds=total_funds_usdt(funding_usdt, current_capital, options_trading_usdc), + total_funds=total_funds_usdt( + funding_usdt, current_capital, options_trading_usdc, options_funding_usdc + ), + options_funding_usdc=options_funding_usdc, options_trading_usdc=options_trading_usdc, trading_day=trading_day, daily_start_capital=DAILY_START_CAPITAL, @@ -6793,13 +6799,16 @@ def api_account_snapshot(): funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS) options_trading_usdc = None + options_funding_usdc = None if OKX_OPTIONS_ENABLED and exchange_options.apiKey: try: - from lib.exchange.okx_options_lib import fetch_options_trading_usdc + from lib.exchange.okx_options_lib import fetch_options_funding_usdc, fetch_options_trading_usdc options_trading_usdc = fetch_options_trading_usdc(exchange_options) + options_funding_usdc = fetch_options_funding_usdc(exchange_options) except Exception: options_trading_usdc = None + options_funding_usdc = None recommended_capital = get_recommended_capital(current_capital) from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors @@ -6842,8 +6851,11 @@ def api_account_snapshot(): return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, + "options_funding_usdc": options_funding_usdc, "options_trading_usdc": options_trading_usdc, - "total_funds": total_funds_usdt(funding_usdt, current_capital, options_trading_usdc), + "total_funds": total_funds_usdt( + funding_usdt, current_capital, options_trading_usdc, options_funding_usdc + ), "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, diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 608a771..d5e4e16 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -2356,8 +2356,16 @@ html[data-theme="light"] .settings-export-link { } .options-strike-table-wrap { overflow-x: auto; + overflow-y: auto; + max-height: 352px; margin-top: 8px; } +.options-strike-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: rgba(18, 24, 38, 0.98); +} .options-strike-table { width: 100%; border-collapse: collapse; @@ -2462,7 +2470,68 @@ html[data-theme="light"] .settings-export-link { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 16px; - align-items: start; + align-items: stretch; +} +.options-order-card, +.options-pos-card-wrap { + display: flex; + flex-direction: column; + min-height: 0; +} +.options-pos-stack { + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; +} +.options-pos-subcard { + display: flex; + flex-direction: column; + min-height: 0; +} +.options-pos-subcard h3 { + margin: 0 0 8px; + font-size: 0.95rem; + font-weight: 600; +} +.options-pos-live-pane { + flex: 1; + min-height: 100px; + max-height: 220px; + overflow-y: auto; +} +.options-pos-stats-card { + flex-shrink: 0; +} +.options-stats-grid { + display: flex; + flex-wrap: wrap; + gap: 16px 24px; +} +.options-stat-item { + display: flex; + flex-direction: column; + gap: 4px; +} +.options-stat-item .k { + font-size: 0.78rem; + opacity: 0.7; +} +.options-stat-item .v { + font-size: 1.1rem; + font-weight: 600; +} +.options-pos-history-card { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} +.options-history-table-wrap { + flex: 1; + overflow-y: auto; + min-height: 120px; } @media (max-width: 1100px) { .options-dual-grid { diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 67bcf6f..4ae1b81 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -9,7 +9,6 @@ optType: "C", chain: null, selectedInst: null, - posTab: "live", }; function fmt(v, d) { @@ -289,7 +288,7 @@ msgEl.textContent = d.ok ? "下单已提交,可在 OKX 委托中查看" : (d.msg || "失败"); msgEl.classList.toggle("opt-error", !d.ok); if (d.ok) { - refreshPositions(); + refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); } else { alert(d.msg || "下单失败"); @@ -349,8 +348,7 @@ } else { alert(r.msg || "平仓失败"); } - refreshPositions(); - refreshHistory(); + refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); } finally { if (btn) btn.disabled = false; @@ -380,6 +378,24 @@ }); } + async function refreshStats() { + const d = await apiJson("/api/options/stats"); + const winEl = document.getElementById("opt-stats-winrate"); + const plrEl = document.getElementById("opt-stats-plr"); + const closedEl = document.getElementById("opt-stats-closed"); + if (!d.ok) { + if (winEl) winEl.textContent = "—"; + if (plrEl) plrEl.textContent = "—"; + if (closedEl) closedEl.textContent = "—"; + return; + } + if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%"; + if (plrEl) { + plrEl.textContent = d.profit_loss_ratio != null ? String(d.profit_loss_ratio) : "—"; + } + if (closedEl) closedEl.textContent = String(d.total_closed || 0); + } + async function refreshHistory() { const d = await apiJson("/api/options/history"); const tbody = document.getElementById("opt-history-tbody"); @@ -405,14 +421,10 @@ }); } - function switchPosTab(tab) { - state.posTab = tab; - document.querySelectorAll(".opt-pos-tab").forEach(function (b) { - b.classList.toggle("active", b.getAttribute("data-tab") === tab); - }); - document.getElementById("opt-pos-live").hidden = tab !== "live"; - document.getElementById("opt-pos-history").hidden = tab !== "history"; - if (tab === "history") refreshHistory(); + function refreshAllPositions() { + refreshPositions(); + refreshStats(); + refreshHistory(); } document.querySelectorAll(".opt-uly-btn").forEach(function (btn) { @@ -433,18 +445,9 @@ }); }); - document.querySelectorAll(".opt-pos-tab").forEach(function (btn) { - btn.addEventListener("click", function () { - switchPosTab(btn.getAttribute("data-tab")); - }); - }); - document.getElementById("opt-exp-select").addEventListener("change", renderStrikes); document.getElementById("opt-load-chain").addEventListener("click", loadChain); - document.getElementById("opt-refresh-positions").addEventListener("click", function () { - refreshPositions(); - if (state.posTab === "history") refreshHistory(); - }); + document.getElementById("opt-refresh-positions").addEventListener("click", refreshAllPositions); document.getElementById("opt-open-btn").addEventListener("click", openPosition); document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) { @@ -464,5 +467,5 @@ updateSizeInputs(); loadChain(); - refreshPositions(); + refreshAllPositions(); })(); diff --git a/lib/common/static/options_settings.js b/lib/common/static/options_settings.js index 2b3938e..4e7f27b 100644 --- a/lib/common/static/options_settings.js +++ b/lib/common/static/options_settings.js @@ -64,10 +64,12 @@ body: JSON.stringify({ ccy: document.getElementById("opt-set-cross-ccy").value, amount: amount, - account: document.getElementById("opt-set-cross-acct").value, + from_account: document.getElementById("opt-set-cross-from-acct").value, + to_account: document.getElementById("opt-set-cross-to-acct").value, direction: document.getElementById("opt-set-cross-dir").value, }), }); setMsg("opt-set-cross-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok); + if (d.ok && typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); }); })(); diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 010210a..a0969b3 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -489,6 +489,14 @@ def fetch_options_trading_usdc(ex: ccxt.okx) -> float | None: return round(float(v), 2) +def fetch_options_funding_usdc(ex: ccxt.okx) -> float | None: + bal = fetch_options_balances(ex) + v = bal.get("funding_usdc") + if v is None: + return None + return round(float(v), 2) + + def spot_market_swap_usdt_usdc( ex: ccxt.okx, *, @@ -538,7 +546,8 @@ def transfer_main_sub_account( amount: float, sub_acct: str, main_to_sub: bool, - account: str = "funding", + from_account: str = "funding", + to_account: str = "funding", ) -> dict[str, Any]: """主账户与子账户之间划转(须主账户 API)。""" if amount <= 0: @@ -546,15 +555,16 @@ def transfer_main_sub_account( sub = (sub_acct or "").strip() if not sub: return {"ok": False, "msg": "未配置子账户名称 OKX_SUB_ACCOUNT_NAME"} - acct_code = _OKX_ACCT_CODE.get((account or "funding").lower(), "6") + from_code = _OKX_ACCT_CODE.get((from_account or "funding").lower(), "6") + to_code = _OKX_ACCT_CODE.get((to_account or "funding").lower(), "6") try: resp = ex.private_post_asset_transfer( { "type": "1" if main_to_sub else "2", "ccy": str(ccy).upper(), "amt": str(amount), - "from": acct_code, - "to": acct_code, + "from": from_code, + "to": to_code, "subAcct": sub, } ) diff --git a/lib/instance/instance_embed_context_lib.py b/lib/instance/instance_embed_context_lib.py index e13ef18..ac971ef 100644 --- a/lib/instance/instance_embed_context_lib.py +++ b/lib/instance/instance_embed_context_lib.py @@ -89,11 +89,14 @@ def total_funds_usdt( funding_usdt: float | None, trading_usdt: float | None, options_trading_usdc: float | None = None, + options_funding_usdc: float | None = None, ) -> float | None: if funding_usdt is None: return None try: total = float(funding_usdt) + float(trading_usdt or 0) + if options_funding_usdc is not None: + total += float(options_funding_usdc) if options_trading_usdc is not None: total += float(options_trading_usdc) return round(total, 2) diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index ee66531..be6ee33 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1061,6 +1061,11 @@ function refreshAccountSnapshot(){ const el = document.getElementById("current-capital"); if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`; } + if (typeof data.options_funding_usdc !== "undefined") { + const el = document.getElementById("options-funding-usdc"); + if (el) el.innerText = (data.options_funding_usdc === null || data.options_funding_usdc === undefined) + ? "—" : `${Number(data.options_funding_usdc).toFixed(2)} USDC`; + } if (typeof data.options_trading_usdc !== "undefined") { const el = document.getElementById("options-trading-usdc"); if (el) el.innerText = (data.options_trading_usdc === null || data.options_trading_usdc === undefined) diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 8af0157..7135058 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -1612,6 +1612,11 @@ function refreshAccountSnapshot(){ const el = document.getElementById("current-capital"); if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`; } + if (typeof data.options_funding_usdc !== "undefined") { + const el = document.getElementById("options-funding-usdc"); + if (el) el.innerText = (data.options_funding_usdc === null || data.options_funding_usdc === undefined) + ? "—" : `${Number(data.options_funding_usdc).toFixed(2)} USDC`; + } if (typeof data.options_trading_usdc !== "undefined") { const el = document.getElementById("options-trading-usdc"); if (el) el.innerText = (data.options_trading_usdc === null || data.options_trading_usdc === undefined) diff --git a/lib/instance/templates/instance_header_panel.html b/lib/instance/templates/instance_header_panel.html index 7fdab80..1312573 100644 --- a/lib/instance/templates/instance_header_panel.html +++ b/lib/instance/templates/instance_header_panel.html @@ -75,6 +75,10 @@