feat条件单候选展示交易户资金,并支持打开实例划转。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -586,13 +586,15 @@ def api_open_url(iid: int):
|
||||
inst = get_instance(iid)
|
||||
if not inst:
|
||||
return jsonify({"ok": False, "msg": "不存在"}), 404
|
||||
tab = (request.args.get("tab") or "").strip() or None
|
||||
url = build_embed_open_url(
|
||||
inst["base_url"],
|
||||
bridge_token=inst["bridge_token"],
|
||||
embed_path=inst.get("embed_path") or "/embed",
|
||||
ttl_sec=3600,
|
||||
tab=tab,
|
||||
)
|
||||
return jsonify({"ok": True, "url": url, "base_url": inst["base_url"]})
|
||||
return jsonify({"ok": True, "url": url, "base_url": inst["base_url"], "tab": tab})
|
||||
|
||||
|
||||
@app.route("/api/settings/update/hub/version", methods=["GET"])
|
||||
|
||||
@@ -33,6 +33,15 @@ def _sse_heartbeat_sec() -> float:
|
||||
return 25.0
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> float | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows or []:
|
||||
@@ -45,6 +54,20 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
mode = str(inst.get("mode") or "").strip().lower()
|
||||
if mode not in ("sim", "live"):
|
||||
mode = ""
|
||||
funds = snap.get("funds") if isinstance(snap.get("funds"), dict) else {}
|
||||
# 开仓吃交易户;优先 avail,回退总额
|
||||
trading_usdt = _safe_float(funds.get("trading_usdt_avail"))
|
||||
if trading_usdt is None:
|
||||
trading_usdt = _safe_float(funds.get("trading_usdt"))
|
||||
if trading_usdt is None:
|
||||
trading_usdt = _safe_float(snap.get("trading_usdt"))
|
||||
funding_usdt = _safe_float(funds.get("funding_usdt"))
|
||||
if funding_usdt is None:
|
||||
funding_usdt = _safe_float(snap.get("funding_usdt"))
|
||||
if trading_usdt is not None:
|
||||
trading_usdt = round(float(trading_usdt), 4)
|
||||
if funding_usdt is not None:
|
||||
funding_usdt = round(float(funding_usdt), 4)
|
||||
out.append(
|
||||
{
|
||||
"id": row.get("id"),
|
||||
@@ -55,6 +78,8 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"selectable": selectable,
|
||||
"mode": mode,
|
||||
"error": row.get("error") or "",
|
||||
"trading_usdt": trading_usdt,
|
||||
"funding_usdt": funding_usdt,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -24,11 +24,18 @@ def build_embed_open_url(
|
||||
bridge_token: str,
|
||||
embed_path: str = "/embed",
|
||||
ttl_sec: int = 120,
|
||||
tab: str | None = None,
|
||||
) -> str:
|
||||
root = (base_url or "").rstrip("/")
|
||||
path = embed_path or "/embed"
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
tab_s = "".join(c for c in str(tab or "").strip().lower() if c.isalnum() or c == "_")[
|
||||
:32
|
||||
]
|
||||
if tab_s:
|
||||
joiner = "&" if "?" in path else "?"
|
||||
path = f"{path}{joiner}tab={tab_s}"
|
||||
token = make_sso_token(bridge_token, ttl_sec=ttl_sec)
|
||||
q = urlencode({"token": token, "next": path})
|
||||
return f"{root}/hub_sso?{q}"
|
||||
|
||||
+56
-1
@@ -202,6 +202,36 @@
|
||||
return m === "sim" || m === "live" ? m : "";
|
||||
}
|
||||
|
||||
function fmtTradingU(v) {
|
||||
if (v == null || v === "") return "—";
|
||||
const n = Number(v);
|
||||
if (Number.isNaN(n)) return "—";
|
||||
return n.toFixed(2) + "U";
|
||||
}
|
||||
|
||||
function openInstanceTab(instanceId, tab) {
|
||||
if (!instanceId) return;
|
||||
const q = tab ? "?tab=" + encodeURIComponent(tab) : "";
|
||||
fetch("/api/instances/" + encodeURIComponent(instanceId) + "/open_url" + q)
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (d) {
|
||||
const url =
|
||||
d && d.url
|
||||
? d.url
|
||||
: "/desk?id=" + encodeURIComponent(instanceId);
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
})
|
||||
.catch(function () {
|
||||
window.open(
|
||||
"/desk?id=" + encodeURIComponent(instanceId),
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCandidateCard(it, checkedIds) {
|
||||
const disabled = !it.selectable;
|
||||
const tip = disabled
|
||||
@@ -215,7 +245,14 @@
|
||||
: "可选";
|
||||
const id = String(it.id);
|
||||
const checked = !disabled && checkedIds.indexOf(id) >= 0;
|
||||
const fundsTip =
|
||||
it.funding_usdt != null
|
||||
? "资金户 " + fmtTradingU(it.funding_usdt)
|
||||
: "";
|
||||
return (
|
||||
'<div class="cond-inst-row' +
|
||||
(disabled ? " is-off" : "") +
|
||||
'">' +
|
||||
'<label class="cond-inst' +
|
||||
(disabled ? " is-off" : "") +
|
||||
'" title="' +
|
||||
@@ -232,7 +269,17 @@
|
||||
"</span>" +
|
||||
'<span class="cond-inst-tag">' +
|
||||
escapeHtml(tip) +
|
||||
"</span></label>"
|
||||
"</span>" +
|
||||
'<span class="cond-inst-funds" title="' +
|
||||
escapeHtml(fundsTip || "交易账户可用") +
|
||||
'">交易 ' +
|
||||
escapeHtml(fmtTradingU(it.trading_usdt)) +
|
||||
"</span>" +
|
||||
"</label>" +
|
||||
'<button type="button" class="btn btn-sm cond-inst-open" data-inst-id="' +
|
||||
encodeURIComponent(id) +
|
||||
'" title="新标签打开期权实例(可划转)">打开</button>' +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,6 +334,14 @@
|
||||
renderCandidateGroup("sim", sim, checkedIds) +
|
||||
renderCandidateGroup("live", live, checkedIds) +
|
||||
renderCandidateGroup("", other, checkedIds);
|
||||
box.querySelectorAll(".cond-inst-open").forEach(function (btn) {
|
||||
btn.addEventListener("click", function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const iid = decodeURIComponent(btn.getAttribute("data-inst-id") || "");
|
||||
openInstanceTab(iid, "options");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderDesktopRows(list) {
|
||||
|
||||
+24
-4
@@ -1524,14 +1524,22 @@ input.readonly {
|
||||
.cond-inst-group-label { margin-bottom: 8px; }
|
||||
.cond-inst-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.cond-inst-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
.cond-inst-row.is-off { opacity: 0.5; }
|
||||
.cond-inst {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -1539,9 +1547,21 @@ input.readonly {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cond-inst input { width: auto; margin: 0; }
|
||||
.cond-inst-name { flex: 1; font-weight: 600; }
|
||||
.cond-inst-tag { color: var(--muted); font-size: 0.75rem; }
|
||||
.cond-inst.is-off { opacity: 0.5; cursor: not-allowed; }
|
||||
.cond-inst-name { flex: 1; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cond-inst-tag { color: var(--muted); font-size: 0.75rem; flex-shrink: 0; }
|
||||
.cond-inst-funds {
|
||||
color: var(--accent-2, #38bdf8);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cond-inst-open {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cond-inst.is-off { cursor: not-allowed; }
|
||||
.cond-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<link rel="icon" href="{{ url_for('static', filename='icons/favicon.ico') }}?v=2" sizes="any">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/apple-touch-icon.png') }}?v=2">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=46">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=47">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
<div class="cond-section">
|
||||
<div class="cond-section-title">客户端</div>
|
||||
<p class="muted" style="margin:0 0 8px;font-size:.76rem">展示交易账户可用;右侧「打开」新标签进期权实例划转,划完后回此页刷新即可。</p>
|
||||
<div id="cf-instances"></div>
|
||||
</div>
|
||||
|
||||
@@ -94,6 +95,6 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='cond_orders.js') }}?v=7"></script>
|
||||
<script src="{{ url_for('static', filename='cond_orders.js') }}?v=8"></script>
|
||||
<script>HubCondOrders.boot();</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""条件单候选:带出交易户资金字段."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCondCandidatesFunds(unittest.TestCase):
|
||||
def test_candidates_include_trading_usdt(self):
|
||||
from lib.cond_orders_cache_lib import _candidates_from_snaps
|
||||
|
||||
rows = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "okx-a",
|
||||
"online": True,
|
||||
"error": "",
|
||||
"snapshot": {
|
||||
"positions_brief": [],
|
||||
"cond_order_locked": False,
|
||||
"instance": {"mode": "live"},
|
||||
"funds": {"trading_usdt": 123.456, "funding_usdt": 50.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "okx-b",
|
||||
"online": False,
|
||||
"error": "timeout",
|
||||
"snapshot": {
|
||||
"positions_brief": [{"inst_id": "x"}],
|
||||
"cond_order_locked": False,
|
||||
"instance": {"mode": "sim"},
|
||||
"funds": {},
|
||||
},
|
||||
},
|
||||
]
|
||||
out = _candidates_from_snaps(rows)
|
||||
self.assertEqual(len(out), 2)
|
||||
self.assertTrue(out[0]["selectable"])
|
||||
self.assertAlmostEqual(out[0]["trading_usdt"], 123.456, places=3)
|
||||
self.assertAlmostEqual(out[0]["funding_usdt"], 50.0, places=3)
|
||||
self.assertFalse(out[1]["selectable"])
|
||||
self.assertIsNone(out[1]["trading_usdt"])
|
||||
|
||||
def test_open_url_tab_appended(self):
|
||||
from lib.sso_lib import build_embed_open_url
|
||||
|
||||
url = build_embed_open_url(
|
||||
"https://okx.example.com",
|
||||
bridge_token="tok",
|
||||
embed_path="/embed",
|
||||
tab="options",
|
||||
)
|
||||
self.assertIn("hub_sso?", url)
|
||||
self.assertIn("next=", url)
|
||||
# next 已 urlencode,解码后应含 tab=options
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
q = parse_qs(urlparse(url).query)
|
||||
next_path = unquote(q.get("next", [""])[0])
|
||||
self.assertIn("tab=options", next_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user