feat: restructure OKX options UI with header funds, settings card, and dual-column layout

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 09:36:00 +08:00
parent 38f6bc240e
commit 090b9c6215
15 changed files with 619 additions and 225 deletions
+58
View File
@@ -2458,4 +2458,62 @@ html[data-theme="light"] .settings-export-link {
.options-order-mode-row input[type="number"] {
width: 88px;
}
.options-dual-grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 16px;
align-items: start;
}
@media (max-width: 1100px) {
.options-dual-grid {
grid-template-columns: 1fr;
}
}
.options-order-card h2,
.options-pos-card-wrap h2 {
margin: 0 0 10px;
}
.options-pos-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.options-pos-head h2 {
margin: 0;
}
.options-pos-tabs {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.opt-pos-tab.active {
border-color: #5b8cff;
color: #cfe0ff;
background: rgba(74, 124, 255, 0.28);
}
.options-pos-pane {
min-height: 200px;
}
.opt-pos-card {
margin-bottom: 10px;
}
.options-settings-block {
margin-bottom: 14px;
}
.options-settings-row {
flex-wrap: wrap;
gap: 8px;
}
.options-settings-hint {
font-size: 0.82rem;
margin: 0 0 8px;
}
.pos-pnl-profit {
color: #7ee787;
}
.pos-pnl-loss {
color: #ff8b8b;
}
+91 -91
View File
@@ -9,7 +9,7 @@
optType: "C",
chain: null,
selectedInst: null,
convertQuoteId: null,
posTab: "live",
};
function fmt(v, d) {
@@ -55,20 +55,17 @@
if (!panel || !instId) return;
const row = document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]');
if (!row) return;
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r === row);
});
document.querySelectorAll(".opt-pick-btn").forEach(function (b) {
const on = b.getAttribute("data-inst") === instId;
b.classList.toggle("active", on);
if (!b.dataset.origText) b.dataset.origText = b.textContent;
if (on) b.textContent = "已选";
document.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
const on = btn.getAttribute("data-inst") === instId;
btn.classList.toggle("active", on);
if (!btn.dataset.origText) btn.dataset.origText = btn.textContent;
if (on) btn.textContent = "已选";
});
const oldInline = document.querySelector(".opt-order-inline-row");
if (oldInline) oldInline.remove();
const tr = document.createElement("tr");
tr.className = "opt-order-inline-row";
const td = document.createElement("td");
@@ -113,15 +110,8 @@
return '<span class="opt-moneyness opt-moneyness-' + m + '">' + label + "</span>";
}
async function refreshBalances() {
const d = await apiJson("/api/options/balances");
if (!d.ok) return;
document.getElementById("opt-funding-usdt").textContent = fmt(d.funding_usdt) + " U";
document.getElementById("opt-funding-usdc").textContent = fmt(d.funding_usdc) + " U";
document.getElementById("opt-trading-usdt").textContent = fmt(d.trading_usdt) + " U";
document.getElementById("opt-trading-usdc").textContent = fmt(d.trading_usdc) + " U";
document.getElementById("opt-trading-usdg").textContent = fmt(d.trading_usdg) + " U";
document.getElementById("opt-trade-budget").textContent = fmt(d.trade_budget) + " USDC";
function optTypeLabel(t) {
return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call";
}
function expLabel(ms) {
@@ -299,8 +289,8 @@
msgEl.textContent = d.ok ? "下单已提交,可在 OKX 委托中查看" : (d.msg || "失败");
msgEl.classList.toggle("opt-error", !d.ok);
if (d.ok) {
refreshBalances();
refreshPositions();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
} else {
alert(d.msg || "下单失败");
}
@@ -309,6 +299,32 @@
}
}
function renderPositionCard(p) {
const upl = p.upl;
const uplCls = upl > 0 ? "pos-pnl-profit" : upl < 0 ? "pos-pnl-loss" : "";
const sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long";
return (
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
'<div class="pos-card-head">' +
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span></div>" +
'<div class="pos-head-actions">' +
'<button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '">限价平仓</button>' +
"</div></div>" +
'<div class="pos-meta">' +
'<span class="pos-meta-item">行权价: ' + fmt(p.strike, 0) + "</span>" +
'<span class="pos-meta-item">张数: ' + fmt(p.pos, 0) + " · 币量 " + fmt(p.eth_amount, 4) + "</span>" +
"</div>" +
'<div class="pos-grid">' +
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + fmt(p.avg_px, 4) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + fmt(p.mark_px, 4) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ' + uplCls + '">' + fmt(p.upl, 4) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
(p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "</span></div>" +
"</div></div>"
);
}
async function closePosition(inst, btn) {
const q = await apiJson("/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=sheets&sheets=1");
if (!q.ok) {
@@ -334,7 +350,8 @@
alert(r.msg || "平仓失败");
}
refreshPositions();
refreshBalances();
refreshHistory();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
} finally {
if (btn) btn.disabled = false;
}
@@ -342,33 +359,62 @@
async function refreshPositions() {
const d = await apiJson("/api/options/positions");
const tbody = document.getElementById("opt-positions-tbody");
tbody.innerHTML = "";
const wrap = document.getElementById("opt-pos-cards");
const empty = document.getElementById("opt-pos-empty");
const list = (d.ok && d.positions) || [];
wrap.innerHTML = "";
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="8" class="muted">暂无持仓</td></tr>';
empty.style.display = "";
return;
}
empty.style.display = "none";
list.forEach(function (p) {
const tr = document.createElement("tr");
tr.innerHTML =
"<td><code>" + (p.inst_id || "") + "</code></td>" +
"<td>" + fmt(p.pos, 0) + "</td>" +
"<td>" + fmt(p.eth_amount, 4) + "</td>" +
"<td>" + fmt(p.avg_px, 4) + "</td>" +
"<td>" + fmt(p.mark_px, 4) + "</td>" +
"<td>" + fmt(p.upl, 4) + "</td>" +
"<td>" + (p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "</td>" +
'<td><button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '">限价平仓</button></td>';
tbody.appendChild(tr);
const div = document.createElement("div");
div.innerHTML = renderPositionCard(p);
wrap.appendChild(div.firstChild);
});
tbody.querySelectorAll(".opt-close-btn").forEach(function (btn) {
wrap.querySelectorAll(".opt-close-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
closePosition(btn.getAttribute("data-inst"), btn);
});
});
}
async function refreshHistory() {
const d = await apiJson("/api/options/history");
const tbody = document.getElementById("opt-history-tbody");
tbody.innerHTML = "";
const list = (d.ok && d.history) || [];
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无历史记录</td></tr>';
return;
}
list.forEach(function (h) {
const tr = document.createElement("tr");
const prem = h.status === "closed" ? h.premium_received : h.premium_paid;
const pnl = h.realized_pnl;
const pnlTxt = pnl != null ? fmt(pnl, 4) : "—";
tr.innerHTML =
"<td><code>" + (h.inst_id || "") + "</code></td>" +
"<td>" + fmt(h.sheets, 0) + "</td>" +
"<td>" + fmt(prem, 4) + "</td>" +
"<td>" + (h.status === "closed" ? "已平" : "持仓中") + "</td>" +
"<td>" + pnlTxt + "</td>" +
"<td>" + (h.closed_at || h.created_at || "—") + "</td>";
tbody.appendChild(tr);
});
}
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();
}
document.querySelectorAll(".opt-uly-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); });
@@ -387,10 +433,18 @@
});
});
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-balances").addEventListener("click", refreshBalances);
document.getElementById("opt-refresh-positions").addEventListener("click", refreshPositions);
document.getElementById("opt-refresh-positions").addEventListener("click", function () {
refreshPositions();
if (state.posTab === "history") refreshHistory();
});
document.getElementById("opt-open-btn").addEventListener("click", openPosition);
document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) {
@@ -408,61 +462,7 @@
});
});
document.getElementById("opt-convert-quote-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-convert-amount").value);
if (!amount || amount <= 0) {
alert("请输入 USDT 数量");
return;
}
const d = await apiJson("/api/options/convert/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: amount }),
});
const prev = document.getElementById("opt-convert-preview");
if (!d.ok) {
prev.textContent = d.msg || "询价失败";
state.convertQuoteId = null;
document.getElementById("opt-convert-exec-btn").disabled = true;
return;
}
state.convertQuoteId = d.quote_id;
prev.textContent =
"预估获得 " + fmt(d.base_sz, 6) + " USDC,汇率 " + fmt(d.cnvt_px, 6);
document.getElementById("opt-convert-exec-btn").disabled = false;
});
document.getElementById("opt-convert-exec-btn").addEventListener("click", async function () {
if (!state.convertQuoteId) return;
const amount = parseFloat(document.getElementById("opt-convert-amount").value);
const d = await apiJson("/api/options/convert/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quote_id: state.convertQuoteId, rfq_sz: amount }),
});
document.getElementById("opt-convert-preview").textContent = d.ok ? "兑换成功" : (d.msg || "失败");
state.convertQuoteId = null;
document.getElementById("opt-convert-exec-btn").disabled = true;
refreshBalances();
});
document.getElementById("opt-transfer-btn").addEventListener("click", async function () {
const d = await apiJson("/api/options/transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-transfer-ccy").value,
from: document.getElementById("opt-transfer-from").value,
to: document.getElementById("opt-transfer-to").value,
amount: parseFloat(document.getElementById("opt-transfer-amount").value),
}),
});
document.getElementById("opt-transfer-msg").textContent = d.ok ? "划转成功" : (d.msg || "失败");
refreshBalances();
});
updateSizeInputs();
refreshBalances();
loadChain();
refreshPositions();
})();
+73
View File
@@ -0,0 +1,73 @@
(function () {
"use strict";
const root = document.getElementById("options-settings-root");
if (!root) return;
async function apiJson(url, opts) {
const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
return r.json();
}
function setMsg(id, text, isErr) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = text || "";
el.classList.toggle("opt-error", !!isErr);
}
document.getElementById("opt-set-swap-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-swap-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-swap-msg", "请输入有效数量", true);
return;
}
const d = await apiJson("/api/options/spot/swap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
direction: document.getElementById("opt-set-swap-dir").value,
amount: amount,
}),
});
setMsg("opt-set-swap-msg", d.ok ? "兑换单已提交" : (d.msg || "失败"), !d.ok);
});
document.getElementById("opt-set-int-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-int-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-int-msg", "请输入有效数量", true);
return;
}
const d = await apiJson("/api/options/transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-set-int-ccy").value,
from: document.getElementById("opt-set-int-from").value,
to: document.getElementById("opt-set-int-to").value,
amount: amount,
}),
});
setMsg("opt-set-int-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
});
document.getElementById("opt-set-cross-btn").addEventListener("click", async function () {
const amount = parseFloat(document.getElementById("opt-set-cross-amount").value);
if (!amount || amount <= 0) {
setMsg("opt-set-cross-msg", "请输入有效数量", true);
return;
}
const d = await apiJson("/api/options/cross-transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ccy: document.getElementById("opt-set-cross-ccy").value,
amount: amount,
account: document.getElementById("opt-set-cross-acct").value,
direction: document.getElementById("opt-set-cross-dir").value,
}),
});
setMsg("opt-set-cross-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
});
})();
+89
View File
@@ -478,6 +478,95 @@ def transfer_ccy(
return {"ok": False, "msg": str(e)}
_OKX_ACCT_CODE = {"funding": "6", "trading": "18", "spot": "18"}
def fetch_options_trading_usdc(ex: ccxt.okx) -> float | None:
bal = fetch_options_balances(ex)
v = bal.get("trading_usdc")
if v is None:
return None
return round(float(v), 2)
def spot_market_swap_usdt_usdc(
ex: ccxt.okx,
*,
direction: str,
amount: float,
) -> dict[str, Any]:
"""现货市价兑换 USDC-USDT。direction: usdt_to_usdc | usdc_to_usdt。"""
if amount <= 0:
return {"ok": False, "msg": "数量须大于 0"}
d = (direction or "").lower()
inst_id = "USDC-USDT"
try:
if d == "usdt_to_usdc":
body = {
"instId": inst_id,
"tdMode": "cash",
"side": "buy",
"ordType": "market",
"sz": str(amount),
"tgtCcy": "quote",
}
elif d == "usdc_to_usdt":
body = {
"instId": inst_id,
"tdMode": "cash",
"side": "sell",
"ordType": "market",
"sz": str(amount),
"tgtCcy": "base",
}
else:
return {"ok": False, "msg": "direction 须为 usdt_to_usdc 或 usdc_to_usdt"}
resp = ex.private_post_trade_order(body)
data = (resp or {}).get("data") or []
if data and str(data[0].get("sCode")) == "0":
return {"ok": True, "data": data[0], "raw": resp}
msg = data[0].get("sMsg") if data else str(resp)
return {"ok": False, "msg": msg or "现货兑换失败", "raw": resp}
except Exception as e:
return {"ok": False, "msg": str(e)}
def transfer_main_sub_account(
ex: ccxt.okx,
*,
ccy: str,
amount: float,
sub_acct: str,
main_to_sub: bool,
account: str = "funding",
) -> dict[str, Any]:
"""主账户与子账户之间划转(须主账户 API)。"""
if amount <= 0:
return {"ok": False, "msg": "划转金额须大于 0"}
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")
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,
"subAcct": sub,
}
)
data = (resp or {}).get("data") or []
if data and str(data[0].get("sCode", "0")) == "0":
return {"ok": True, "data": data[0], "raw": resp}
msg = data[0].get("sMsg") if data else str(resp)
return {"ok": False, "msg": msg or "主/子账户划转失败", "raw": resp}
except Exception as e:
return {"ok": False, "msg": str(e)}
def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str, Any]:
sheets = _safe_float(pos.get("pos")) or 0.0
avg = _safe_float(pos.get("avgPx"))
+9 -2
View File
@@ -85,11 +85,18 @@ def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float
return profit_loss_ratio_from_averages(avg_win, avg_loss)
def total_funds_usdt(funding_usdt: float | None, trading_usdt: float | None) -> float | None:
def total_funds_usdt(
funding_usdt: float | None,
trading_usdt: float | None,
options_trading_usdc: float | None = None,
) -> float | None:
if funding_usdt is None:
return None
try:
return round(float(funding_usdt) + float(trading_usdt or 0), 2)
total = float(funding_usdt) + float(trading_usdt or 0)
if options_trading_usdc is not None:
total += float(options_trading_usdc)
return round(total, 2)
except (TypeError, ValueError):
return None
+11 -5
View File
@@ -139,18 +139,21 @@ def build_instance_settings_view(
opt_key = (os.getenv("OKX_OPTIONS_API_KEY") or "").strip()
sections.append(
{
"title": "期权账户(主账户)",
"title": "期权设置",
"rows": [
_row("期权模块", "已启用"),
_row(
"期权 API",
f"已配置(…{opt_key[-4:]}" if len(opt_key) >= 4 else "未配置",
),
_row("单笔权利金上限", f"{_env_float('OKX_OPTIONS_TRADE_BUDGET_USDC', 10):g} USDC"),
_row(
"资金说明",
"资金账户 USDT 兑换 USDC 后划转到交易账户",
"期权页操作;与永续子账户资金分开",
"子账户",
(os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip() or "未配置 OKX_SUB_ACCOUNT_NAME",
"主/子账户划转用",
),
_row(
"说明",
"币种兑换与账户划转到右侧「期权设置」卡片操作",
),
],
}
@@ -169,6 +172,9 @@ def build_instance_settings_view(
"sections": sections,
"data_export_version": int(data_export_version),
"show_transfer": (exchange_key or "").strip().lower() in ("gate", "binance", "okx"),
"options_settings_enabled": (exchange_key or "").strip().lower() == "okx"
and _env_bool("OKX_OPTIONS_ENABLED", False),
"options_sub_account": (os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip(),
"auto_transfer_enabled": auto_transfer_on,
"auto_transfer_bj_hour": _env_int("AUTO_TRANSFER_BJ_HOUR", 8),
"auto_transfer_amount": _env_float("AUTO_TRANSFER_AMOUNT", 30),
@@ -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_trading_usdc !== "undefined") {
const el = document.getElementById("options-trading-usdc");
if (el) el.innerText = (data.options_trading_usdc === null || data.options_trading_usdc === undefined)
? "—" : `${Number(data.options_trading_usdc).toFixed(2)} USDC`;
}
if (typeof data.unrealized_pnl !== "undefined") {
paintRealtimePnl(data.unrealized_pnl);
}
+6 -1
View File
@@ -68,7 +68,7 @@
</div>
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
{% if page != 'settings' and page != 'options' %}
{% if page != 'settings' %}
{% include 'instance_header_panel.html' %}
{% endif %}
{% if page != 'settings' and page != 'options' %}
@@ -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_trading_usdc !== "undefined") {
const el = document.getElementById("options-trading-usdc");
if (el) el.innerText = (data.options_trading_usdc === null || data.options_trading_usdc === undefined)
? "—" : `${Number(data.options_trading_usdc).toFixed(2)} USDC`;
}
if (typeof data.unrealized_pnl !== "undefined") {
paintRealtimePnl(data.unrealized_pnl);
}
@@ -74,6 +74,12 @@
<div class="label">交易账户</div>
<div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div>
</div>
{% if options_enabled %}
<div class="stat-strip-item">
<div class="label">期权交易账户</div>
<div class="value" id="options-trading-usdc">{% if options_trading_usdc is not none %}{{ funds_fmt(options_trading_usdc) }} USDC{% else %}—{% endif %}</div>
</div>
{% endif %}
<div class="stat-strip-item stat-strip-item--pnl">
<div class="label">实时盈亏</div>
<div class="value" id="realtime-pnl"></div>
@@ -39,6 +39,13 @@
</div>
<div class="settings-side-col">
{% if instance_settings.options_settings_enabled %}
<div class="card settings-card">
<h2>期权设置</h2>
<p class="settings-card-desc">主账户期权资金:现货市价兑换 USDT/USDC;主账户与子账户(永续)之间划转。</p>
{% include 'options_settings_panel.html' %}
</div>
{% endif %}
{% if instance_settings.show_transfer %}
<div class="card settings-card">
<h2>资金划转</h2>
+88
View File
@@ -70,11 +70,14 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
place_option_limit_order,
place_option_market_order,
quote_option_contract,
spot_market_swap_usdt_usdc,
transfer_ccy,
transfer_main_sub_account,
)
return {
"enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
"sub_account_name": (os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip(),
"get_db": app_module.get_db,
"login_required": app_module.login_required,
"exchange_options": getattr(app_module, "exchange_options", None),
@@ -101,6 +104,8 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
"estimate_usdt_to_usdc": estimate_usdt_to_usdc,
"execute_convert": execute_convert,
"transfer_ccy": transfer_ccy,
"spot_market_swap_usdt_usdc": spot_market_swap_usdt_usdc,
"transfer_main_sub_account": transfer_main_sub_account,
"options_api_ready": options_api_ready,
}
@@ -452,6 +457,89 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
conn.close()
return jsonify(result)
@app.route("/api/options/spot/swap", methods=["POST"])
@lr
def api_options_spot_swap():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
direction = (data.get("direction") or "usdt_to_usdc").strip()
try:
amount = float(data.get("amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "数量无效"})
return jsonify(cfg["spot_market_swap_usdt_usdc"](ex, direction=direction, amount=amount))
@app.route("/api/options/cross-transfer", methods=["POST"])
@lr
def api_options_cross_transfer():
ex, err = _require_options_ex(cfg)
if ex is 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()
try:
amount = float(data.get("amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "数量无效"})
main_to_sub = direction == "main_to_sub"
result = cfg["transfer_main_sub_account"](
ex,
ccy=ccy,
amount=amount,
sub_acct=cfg.get("sub_account_name") or "",
main_to_sub=main_to_sub,
account=account,
)
if result.get("ok"):
conn = cfg["get_db"]()
try:
init_options_tables(conn)
conn.execute(
"""
INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message)
VALUES (?, ?, ?, ?, 'ok', ?)
""",
(
ccy,
amount,
"main" if main_to_sub else "sub",
"sub" if main_to_sub else "main",
f"cross:{account}",
),
)
conn.commit()
finally:
conn.close()
return jsonify(result)
@app.route("/api/options/history")
@lr
def api_options_history():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
conn = cfg["get_db"]()
try:
init_options_tables(conn)
rows = conn.execute(
"""
SELECT id, inst_id, underlying, opt_type, strike, sheets, eth_amount,
open_quote, premium_paid, close_quote, premium_received,
realized_pnl, status, signal_note, created_at, closed_at
FROM options_trades
ORDER BY id DESC
LIMIT 200
"""
).fetchall()
items = [dict(r) for r in rows]
finally:
conn.close()
return jsonify({"ok": True, "history": items})
def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
if app.extensions.get("options_monitor_started"):
+85 -124
View File
@@ -1,135 +1,96 @@
<div class="card options-page-card" style="grid-column:1/-1" id="options-root"
data-trade-budget="{{ options_trade_budget | default(10) }}"
<div class="options-page-wrap" style="grid-column:1/-1" id="options-root"
data-default-underly="{{ options_default_underly | default('ETH') }}">
<h2>期权(USDⓈ 本位 · 仅买方)</h2>
{% if not options_enabled %}
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code> 及主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code></div>
{% endif %}
<p class="muted options-hint">资金账户兑换 USDT→USDC 后,划转到交易账户即可买入。报价单位为每 1 ETH/BTC1 张 = 0.01 ETH/BTC。期权链展示近 <span id="opt-chain-dte">14</span> 日到期合约,标注实值/虚值;下单可指定张数。</p>
<div class="options-funds-grid">
<div class="options-funds-col">
<h3>资金账户</h3>
<div class="options-fund-row"><span>USDT</span><strong id="opt-funding-usdt"></strong></div>
<div class="options-fund-row"><span>USDC</span><strong id="opt-funding-usdc"></strong></div>
</div>
<div class="options-funds-col">
<h3>交易账户</h3>
<div class="options-fund-row"><span>USDT</span><strong id="opt-trading-usdt"></strong></div>
<div class="options-fund-row"><span>USDC</span><strong id="opt-trading-usdc"></strong></div>
<div class="options-fund-row"><span>USDG</span><strong id="opt-trading-usdg"></strong></div>
</div>
<div class="options-funds-col options-funds-meta">
<div class="options-fund-row"><span>单笔权利金上限</span><strong id="opt-trade-budget"></strong></div>
<button type="button" class="btn-secondary" id="opt-refresh-balances">刷新余额</button>
</div>
</div>
<div class="options-section card-nested">
<h3>币种兑换(资金账户 USDT → USDC)</h3>
<div class="form-row options-convert-row">
<input type="number" id="opt-convert-amount" min="0" step="0.01" placeholder="USDT 数量">
<button type="button" class="btn-secondary" id="opt-convert-quote-btn">询价</button>
<button type="button" class="btn-primary" id="opt-convert-exec-btn" disabled>确认兑换</button>
</div>
<div id="opt-convert-preview" class="muted"></div>
</div>
<div class="options-section card-nested">
<h3>账户划转</h3>
<div class="form-row options-transfer-row">
<select id="opt-transfer-ccy">
<option value="USDC" selected>USDC</option>
<option value="USDT">USDT</option>
</select>
<select id="opt-transfer-from">
<option value="funding" selected>资金账户</option>
<option value="trading">交易账户</option>
</select>
<span></span>
<select id="opt-transfer-to">
<option value="trading" selected>交易账户</option>
<option value="funding">资金账户</option>
</select>
<input type="number" id="opt-transfer-amount" min="0" step="0.01" placeholder="数量">
<button type="button" class="btn-primary" id="opt-transfer-btn">确认划转</button>
</div>
<div id="opt-transfer-msg" class="muted"></div>
</div>
<div class="options-section">
<div class="form-row options-chain-toolbar">
<button type="button" class="btn-secondary opt-uly-btn active" data-uly="ETH">ETH</button>
<button type="button" class="btn-secondary opt-uly-btn" data-uly="BTC">BTC</button>
<select id="opt-exp-select"><option value="">选择到期日</option></select>
<button type="button" class="btn-secondary opt-type-btn active" data-type="C">看涨 Call</button>
<button type="button" class="btn-secondary opt-type-btn" data-type="P">看跌 Put</button>
<button type="button" class="btn-secondary" id="opt-load-chain">刷新链</button>
</div>
<div id="opt-index-line" class="muted"></div>
<div class="options-strike-table-wrap">
<table class="options-strike-table" id="opt-strike-table">
<thead>
<tr>
<th>行权价</th>
<th>类型</th>
<th>合约</th>
<th>卖一</th>
<th>买一</th>
<th>操作</th>
</tr>
</thead>
<tbody id="opt-strike-tbody">
<tr><td colspan="6" class="muted">请选择到期日</td></tr>
</tbody>
</table>
</div>
<div id="opt-order-panel-host" class="opt-order-panel-host" hidden aria-hidden="true">
<div id="opt-order-panel" class="opt-order-panel-inner" style="display:none">
<h3 class="opt-order-title">下单</h3>
<div id="opt-order-inst" class="options-order-inst"></div>
<div class="options-order-grid">
<div><span class="k">卖一(每1币)</span><span id="opt-order-ask" class="v"></span></div>
<div><span class="k">张数</span><span id="opt-order-sheets" class="v"></span></div>
<div><span class="k">ETH/BTC 数量</span><span id="opt-order-eth" class="v"></span></div>
<div><span class="k">预估权利金</span><span id="opt-order-premium" class="v"></span></div>
<div class="options-dual-grid">
<div class="card options-order-card">
<h2>期权下单</h2>
<p class="muted options-hint">报价单位为每 1 ETH/BTC1 张 = 0.01。链展示近 <span id="opt-chain-dte">14</span> 日到期,标注实值/虚值。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。</p>
<div class="form-row options-chain-toolbar">
<button type="button" class="btn-secondary opt-uly-btn active" data-uly="ETH">ETH</button>
<button type="button" class="btn-secondary opt-uly-btn" data-uly="BTC">BTC</button>
<select id="opt-exp-select"><option value="">选择到期日</option></select>
<button type="button" class="btn-secondary opt-type-btn active" data-type="C">看涨 Call</button>
<button type="button" class="btn-secondary opt-type-btn" data-type="P">看跌 Put</button>
<button type="button" class="btn-secondary" id="opt-load-chain">刷新链</button>
</div>
<div id="opt-index-line" class="muted"></div>
<div class="options-strike-table-wrap">
<table class="options-strike-table" id="opt-strike-table">
<thead>
<tr>
<th>行权价</th>
<th>类型</th>
<th>合约</th>
<th>卖一</th>
<th>买一</th>
<th>操作</th>
</tr>
</thead>
<tbody id="opt-strike-tbody">
<tr><td colspan="6" class="muted">请选择到期日</td></tr>
</tbody>
</table>
</div>
<div id="opt-order-panel-host" class="opt-order-panel-host" hidden aria-hidden="true">
<div id="opt-order-panel" class="opt-order-panel-inner" style="display:none">
<h3 class="opt-order-title">下单</h3>
<div id="opt-order-inst" class="options-order-inst"></div>
<div class="options-order-grid">
<div><span class="k">卖一(每1币)</span><span id="opt-order-ask" class="v"></span></div>
<div><span class="k">张数</span><span id="opt-order-sheets" class="v"></span></div>
<div><span class="k">ETH/BTC 数量</span><span id="opt-order-eth" class="v"></span></div>
<div><span class="k">预估权利金</span><span id="opt-order-premium" class="v"></span></div>
</div>
<div class="form-row options-order-mode-row">
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
<label><input type="radio" name="opt-size-mode" value="budget_full"> 按单笔上限打满</label>
<label><input type="radio" name="opt-size-mode" value="eth_amount"> 指定币数量</label>
<input type="number" id="opt-eth-amount" min="0.01" step="0.01" placeholder="如 0.5" style="display:none">
<input type="text" id="opt-signal-note" placeholder="备注(关键位说明)">
<button type="button" class="btn-primary" id="opt-open-btn">限价买入 @ 卖一</button>
</div>
<div id="opt-order-msg" class="muted"></div>
</div>
<div class="form-row options-order-mode-row">
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
<label><input type="radio" name="opt-size-mode" value="budget_full"> 按单笔上限打满</label>
<label><input type="radio" name="opt-size-mode" value="eth_amount"> 指定币数量</label>
<input type="number" id="opt-eth-amount" min="0.01" step="0.01" placeholder="如 0.5" style="display:none">
<input type="text" id="opt-signal-note" placeholder="备注(关键位说明)">
<button type="button" class="btn-primary" id="opt-open-btn">限价买入 @ 卖一</button>
</div>
</div>
<div class="card options-pos-card-wrap">
<div class="options-pos-head">
<h2>持仓</h2>
<button type="button" class="btn-secondary" id="opt-refresh-positions">刷新</button>
</div>
<div class="options-pos-tabs" role="tablist">
<button type="button" class="btn-secondary opt-pos-tab active" data-tab="live">当前持仓</button>
<button type="button" class="btn-secondary opt-pos-tab" data-tab="history">期权历史</button>
</div>
<div id="opt-pos-live" class="panel-scroll pos-list options-pos-pane">
<div class="pos-empty" id="opt-pos-empty">暂无持仓</div>
<div id="opt-pos-cards"></div>
</div>
<div id="opt-pos-history" class="options-pos-pane" hidden>
<div class="options-history-table-wrap">
<table class="options-strike-table" id="opt-history-table">
<thead>
<tr>
<th>合约</th>
<th>张数</th>
<th>权利金</th>
<th>状态</th>
<th>盈亏</th>
<th>时间</th>
</tr>
</thead>
<tbody id="opt-history-tbody">
<tr><td colspan="6" class="muted">加载中…</td></tr>
</tbody>
</table>
</div>
<div id="opt-order-msg" class="muted"></div>
</div>
</div>
</div>
<div class="options-section">
<h3>持仓</h3>
<button type="button" class="btn-secondary" id="opt-refresh-positions">刷新持仓</button>
<div class="options-strike-table-wrap">
<table class="options-strike-table" id="opt-positions-table">
<thead>
<tr>
<th>合约</th>
<th>张数</th>
<th>币量</th>
<th>开仓均价</th>
<th>标记价</th>
<th>浮盈</th>
<th>收益率</th>
<th>操作</th>
</tr>
</thead>
<tbody id="opt-positions-tbody">
<tr><td colspan="8" class="muted">暂无持仓</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="/static/options_panel.js?v=4"></script>
<script src="/static/options_panel.js?v=5"></script>
@@ -0,0 +1,61 @@
<div class="options-settings-panel" id="options-settings-root"
data-sub-account="{{ instance_settings.options_sub_account | default('', true) }}">
<div class="options-settings-block card-nested">
<h3>币种兑换(现货市价)</h3>
<p class="muted options-settings-hint">主账户资金账户:USDT ↔ USDC,走 USDC-USDT 现货市价单。</p>
<div class="form-row options-settings-row">
<select id="opt-set-swap-dir">
<option value="usdt_to_usdc" selected>USDT → USDC</option>
<option value="usdc_to_usdt">USDC → USDT</option>
</select>
<input type="number" id="opt-set-swap-amount" min="0.01" step="0.01" placeholder="数量">
<button type="button" class="btn-primary" id="opt-set-swap-btn">市价兑换</button>
</div>
<div id="opt-set-swap-msg" class="muted"></div>
</div>
<div class="options-settings-block card-nested">
<h3>主账户内划转</h3>
<div class="form-row options-settings-row">
<select id="opt-set-int-ccy">
<option value="USDC" selected>USDC</option>
<option value="USDT">USDT</option>
</select>
<select id="opt-set-int-from">
<option value="funding" selected>资金账户</option>
<option value="trading">交易账户</option>
</select>
<span></span>
<select id="opt-set-int-to">
<option value="trading" selected>交易账户</option>
<option value="funding">资金账户</option>
</select>
<input type="number" id="opt-set-int-amount" min="0.01" step="0.01" placeholder="数量">
<button type="button" class="btn-primary" id="opt-set-int-btn">确认划转</button>
</div>
<div id="opt-set-int-msg" class="muted"></div>
</div>
<div class="options-settings-block card-nested">
<h3>主账户 ↔ 子账户划转</h3>
<p class="muted options-settings-hint">子账户:{{ instance_settings.options_sub_account or '未配置 OKX_SUB_ACCOUNT_NAME' }}</p>
<div class="form-row options-settings-row">
<select id="opt-set-cross-ccy">
<option value="USDT" selected>USDT</option>
<option value="USDC">USDC</option>
</select>
<select id="opt-set-cross-acct">
<option value="funding" selected>资金账户</option>
<option value="trading">交易账户</option>
</select>
<select id="opt-set-cross-dir">
<option value="sub_to_main" selected>子账户 → 主账户</option>
<option value="main_to_sub">主账户 → 子账户</option>
</select>
<input type="number" id="opt-set-cross-amount" min="0.01" step="0.01" placeholder="数量">
<button type="button" class="btn-primary" id="opt-set-cross-btn">确认划转</button>
</div>
<div id="opt-set-cross-msg" class="muted"></div>
</div>
</div>
<script src="/static/options_settings.js?v=1"></script>