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 @@
{{ funds_fmt(current_capital) }}U
{% if options_enabled %} +
+
期权资金账户
+
{% if options_funding_usdc is not none %}{{ funds_fmt(options_funding_usdc) }} USDC{% else %}—{% endif %}
+
期权交易账户
{% if options_trading_usdc is not none %}{{ funds_fmt(options_trading_usdc) }} USDC{% else %}—{% endif %}
diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 2dc8d8a..62011ed 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -479,8 +479,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: return jsonify({"ok": False, "msg": err}) data = request.get_json(silent=True) or {} ccy = (data.get("ccy") or "USDT").upper() - account = (data.get("account") or "funding").strip() direction = (data.get("direction") or "sub_to_main").strip() + from_account = (data.get("from_account") or data.get("account") or "funding").strip() + to_account = (data.get("to_account") or data.get("account") or "funding").strip() try: amount = float(data.get("amount")) except (TypeError, ValueError): @@ -492,7 +493,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: amount=amount, sub_acct=cfg.get("sub_account_name") or "", main_to_sub=main_to_sub, - account=account, + from_account=from_account, + to_account=to_account, ) if result.get("ok"): conn = cfg["get_db"]() @@ -506,9 +508,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: ( ccy, amount, - "main" if main_to_sub else "sub", - "sub" if main_to_sub else "main", - f"cross:{account}", + ("main" if main_to_sub else "sub") + ":" + from_account, + ("sub" if main_to_sub else "main") + ":" + to_account, + "cross", ), ) conn.commit() @@ -540,6 +542,46 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: conn.close() return jsonify({"ok": True, "history": items}) + @app.route("/api/options/stats") + @lr + def api_options_stats(): + ex, err = _require_options_ex(cfg) + if ex is None: + return jsonify({"ok": False, "msg": err}) + from lib.instance.instance_embed_context_lib import profit_loss_ratio_from_averages + + conn = cfg["get_db"]() + try: + init_options_tables(conn) + rows = conn.execute( + """ + SELECT realized_pnl FROM options_trades + WHERE status = 'closed' AND realized_pnl IS NOT NULL + """ + ).fetchall() + finally: + conn.close() + wins: list[float] = [] + losses: list[float] = [] + for row in rows: + pnl = float(row["realized_pnl"]) + if pnl > 0: + wins.append(pnl) + elif pnl < 0: + losses.append(pnl) + total_closed = len(wins) + len(losses) + win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0 + avg_win = sum(wins) / len(wins) if wins else None + avg_loss = sum(losses) / len(losses) if losses else None + return jsonify( + { + "ok": True, + "total_closed": total_closed, + "win_rate": win_rate, + "profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss), + } + ) + def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None: if app.extensions.get("options_monitor_started"): diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 6a2d06b..b5309f5 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -63,34 +63,53 @@

持仓

-
- - -
-
-
暂无持仓
-
-
- - + diff --git a/lib/options/templates/options_settings_panel.html b/lib/options/templates/options_settings_panel.html index 90a2fc2..a355952 100644 --- a/lib/options/templates/options_settings_panel.html +++ b/lib/options/templates/options_settings_panel.html @@ -44,10 +44,15 @@ - + +