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)
|
inst = get_instance(iid)
|
||||||
if not inst:
|
if not inst:
|
||||||
return jsonify({"ok": False, "msg": "不存在"}), 404
|
return jsonify({"ok": False, "msg": "不存在"}), 404
|
||||||
|
tab = (request.args.get("tab") or "").strip() or None
|
||||||
url = build_embed_open_url(
|
url = build_embed_open_url(
|
||||||
inst["base_url"],
|
inst["base_url"],
|
||||||
bridge_token=inst["bridge_token"],
|
bridge_token=inst["bridge_token"],
|
||||||
embed_path=inst.get("embed_path") or "/embed",
|
embed_path=inst.get("embed_path") or "/embed",
|
||||||
ttl_sec=3600,
|
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"])
|
@app.route("/api/settings/update/hub/version", methods=["GET"])
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ def _sse_heartbeat_sec() -> float:
|
|||||||
return 25.0
|
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]]:
|
def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for row in rows or []:
|
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()
|
mode = str(inst.get("mode") or "").strip().lower()
|
||||||
if mode not in ("sim", "live"):
|
if mode not in ("sim", "live"):
|
||||||
mode = ""
|
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(
|
out.append(
|
||||||
{
|
{
|
||||||
"id": row.get("id"),
|
"id": row.get("id"),
|
||||||
@@ -55,6 +78,8 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
"selectable": selectable,
|
"selectable": selectable,
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
"error": row.get("error") or "",
|
"error": row.get("error") or "",
|
||||||
|
"trading_usdt": trading_usdt,
|
||||||
|
"funding_usdt": funding_usdt,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -24,11 +24,18 @@ def build_embed_open_url(
|
|||||||
bridge_token: str,
|
bridge_token: str,
|
||||||
embed_path: str = "/embed",
|
embed_path: str = "/embed",
|
||||||
ttl_sec: int = 120,
|
ttl_sec: int = 120,
|
||||||
|
tab: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
root = (base_url or "").rstrip("/")
|
root = (base_url or "").rstrip("/")
|
||||||
path = embed_path or "/embed"
|
path = embed_path or "/embed"
|
||||||
if not path.startswith("/"):
|
if not path.startswith("/"):
|
||||||
path = "/" + path
|
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)
|
token = make_sso_token(bridge_token, ttl_sec=ttl_sec)
|
||||||
q = urlencode({"token": token, "next": path})
|
q = urlencode({"token": token, "next": path})
|
||||||
return f"{root}/hub_sso?{q}"
|
return f"{root}/hub_sso?{q}"
|
||||||
|
|||||||
+56
-1
@@ -202,6 +202,36 @@
|
|||||||
return m === "sim" || m === "live" ? m : "";
|
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) {
|
function renderCandidateCard(it, checkedIds) {
|
||||||
const disabled = !it.selectable;
|
const disabled = !it.selectable;
|
||||||
const tip = disabled
|
const tip = disabled
|
||||||
@@ -215,7 +245,14 @@
|
|||||||
: "可选";
|
: "可选";
|
||||||
const id = String(it.id);
|
const id = String(it.id);
|
||||||
const checked = !disabled && checkedIds.indexOf(id) >= 0;
|
const checked = !disabled && checkedIds.indexOf(id) >= 0;
|
||||||
|
const fundsTip =
|
||||||
|
it.funding_usdt != null
|
||||||
|
? "资金户 " + fmtTradingU(it.funding_usdt)
|
||||||
|
: "";
|
||||||
return (
|
return (
|
||||||
|
'<div class="cond-inst-row' +
|
||||||
|
(disabled ? " is-off" : "") +
|
||||||
|
'">' +
|
||||||
'<label class="cond-inst' +
|
'<label class="cond-inst' +
|
||||||
(disabled ? " is-off" : "") +
|
(disabled ? " is-off" : "") +
|
||||||
'" title="' +
|
'" title="' +
|
||||||
@@ -232,7 +269,17 @@
|
|||||||
"</span>" +
|
"</span>" +
|
||||||
'<span class="cond-inst-tag">' +
|
'<span class="cond-inst-tag">' +
|
||||||
escapeHtml(tip) +
|
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("sim", sim, checkedIds) +
|
||||||
renderCandidateGroup("live", live, checkedIds) +
|
renderCandidateGroup("live", live, checkedIds) +
|
||||||
renderCandidateGroup("", other, 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) {
|
function renderDesktopRows(list) {
|
||||||
|
|||||||
+24
-4
@@ -1524,14 +1524,22 @@ input.readonly {
|
|||||||
.cond-inst-group-label { margin-bottom: 8px; }
|
.cond-inst-group-label { margin-bottom: 8px; }
|
||||||
.cond-inst-grid {
|
.cond-inst-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
.cond-inst-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.cond-inst-row.is-off { opacity: 0.5; }
|
||||||
.cond-inst {
|
.cond-inst {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -1539,9 +1547,21 @@ input.readonly {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.cond-inst input { width: auto; margin: 0; }
|
.cond-inst input { width: auto; margin: 0; }
|
||||||
.cond-inst-name { flex: 1; font-weight: 600; }
|
.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; }
|
.cond-inst-tag { color: var(--muted); font-size: 0.75rem; flex-shrink: 0; }
|
||||||
.cond-inst.is-off { opacity: 0.5; cursor: not-allowed; }
|
.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 {
|
.cond-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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="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="apple-touch-icon" href="{{ url_for('static', filename='icons/apple-touch-icon.png') }}?v=2">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<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>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
|
|
||||||
<div class="cond-section">
|
<div class="cond-section">
|
||||||
<div class="cond-section-title">客户端</div>
|
<div class="cond-section-title">客户端</div>
|
||||||
|
<p class="muted" style="margin:0 0 8px;font-size:.76rem">展示交易账户可用;右侧「打开」新标签进期权实例划转,划完后回此页刷新即可。</p>
|
||||||
<div id="cf-instances"></div>
|
<div id="cf-instances"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -94,6 +95,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% 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>
|
<script>HubCondOrders.boot();</script>
|
||||||
{% endblock %}
|
{% 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