feat条件单页本页划入交易户,避免跳转丢失新建表单。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -597,6 +597,43 @@ def api_open_url(iid: int):
|
|||||||
return jsonify({"ok": True, "url": url, "base_url": inst["base_url"], "tab": tab})
|
return jsonify({"ok": True, "url": url, "base_url": inst["base_url"], "tab": tab})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/instances/<int:iid>/transfer", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_instance_transfer(iid: int):
|
||||||
|
"""条件单页本页划转:中控发指令,实例执行资金户↔交易户."""
|
||||||
|
from lib.bridge_client_lib import post_instance_transfer
|
||||||
|
|
||||||
|
inst = get_instance(iid)
|
||||||
|
if not inst:
|
||||||
|
return jsonify({"ok": False, "msg": "不存在"}), 404
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
amount = float(data.get("amount"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({"ok": False, "msg": "数量无效"}), 400
|
||||||
|
if amount <= 0:
|
||||||
|
return jsonify({"ok": False, "msg": "数量须大于 0"}), 400
|
||||||
|
from_acct = (data.get("from") or "funding").strip() or "funding"
|
||||||
|
to_acct = (data.get("to") or "trading").strip() or "trading"
|
||||||
|
ccy = (data.get("ccy") or "USDT").strip().upper() or "USDT"
|
||||||
|
out = post_instance_transfer(
|
||||||
|
inst,
|
||||||
|
amount=amount,
|
||||||
|
from_acct=from_acct,
|
||||||
|
to_acct=to_acct,
|
||||||
|
ccy=ccy,
|
||||||
|
)
|
||||||
|
# 成功后轻量刷新总览/条件单缓存,供下次读到新余额;不整页重载
|
||||||
|
if out.get("ok"):
|
||||||
|
try:
|
||||||
|
overview_store.request_refresh()
|
||||||
|
cond_orders_store.request_refresh()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
code = 200 if out.get("ok") else 400
|
||||||
|
return jsonify(out), code
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/settings/update/hub/version", methods=["GET"])
|
@app.route("/api/settings/update/hub/version", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_hub_self_version():
|
def api_hub_self_version():
|
||||||
|
|||||||
@@ -325,6 +325,61 @@ def pull_all_stats(window: str = "7d") -> list[dict[str, Any]]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def post_instance_transfer(
|
||||||
|
inst: dict[str, Any],
|
||||||
|
*,
|
||||||
|
amount: float,
|
||||||
|
from_acct: str = "funding",
|
||||||
|
to_acct: str = "trading",
|
||||||
|
ccy: str = "USDT",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""中控指令:实例账户内划转."""
|
||||||
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
|
token = (inst.get("bridge_token") or "").strip()
|
||||||
|
out: dict[str, Any] = {
|
||||||
|
"ok": False,
|
||||||
|
"msg": "",
|
||||||
|
"instance_id": inst.get("id"),
|
||||||
|
"instance_name": inst.get("name"),
|
||||||
|
}
|
||||||
|
if not base or not token:
|
||||||
|
out["msg"] = "缺少 URL 或 Token"
|
||||||
|
return out
|
||||||
|
url = f"{base}/api/hub/options/transfer"
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
headers=_headers(token),
|
||||||
|
json={
|
||||||
|
"amount": float(amount),
|
||||||
|
"from": from_acct,
|
||||||
|
"to": to_acct,
|
||||||
|
"ccy": ccy,
|
||||||
|
},
|
||||||
|
timeout=max(_timeout(), 30.0),
|
||||||
|
)
|
||||||
|
data, err = _response_json(r)
|
||||||
|
if data is None:
|
||||||
|
out["msg"] = err or f"HTTP {r.status_code}"
|
||||||
|
return out
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
out["msg"] = f"HTTP {r.status_code}"
|
||||||
|
return out
|
||||||
|
out.update(data)
|
||||||
|
out["instance_id"] = inst.get("id")
|
||||||
|
out["instance_name"] = inst.get("name")
|
||||||
|
if r.status_code >= 400 and not out.get("ok"):
|
||||||
|
out["ok"] = False
|
||||||
|
out.setdefault("msg", err or f"HTTP {r.status_code}")
|
||||||
|
return out
|
||||||
|
except requests.Timeout:
|
||||||
|
out["msg"] = "划转超时"
|
||||||
|
return out
|
||||||
|
except Exception as e:
|
||||||
|
out["msg"] = str(e) or "划转失败"
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def fetch_one_pnl_curve(inst: dict[str, Any], *, range_key: str = "7d") -> dict[str, Any]:
|
def fetch_one_pnl_curve(inst: dict[str, Any], *, range_key: str = "7d") -> dict[str, Any]:
|
||||||
"""拉取单机累计已实现盈亏曲线."""
|
"""拉取单机累计已实现盈亏曲线."""
|
||||||
base = (inst.get("base_url") or "").rstrip("/")
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
trading_usdt = round(float(trading_usdt), 4)
|
trading_usdt = round(float(trading_usdt), 4)
|
||||||
if funding_usdt is not None:
|
if funding_usdt is not None:
|
||||||
funding_usdt = round(float(funding_usdt), 4)
|
funding_usdt = round(float(funding_usdt), 4)
|
||||||
|
transfer_blocked = bool(snap.get("transfer_blocked")) or has_pos
|
||||||
|
transfer_block_msg = str(snap.get("transfer_block_msg") or "").strip()
|
||||||
|
if has_pos and not transfer_block_msg:
|
||||||
|
transfer_block_msg = "期权持仓中禁止划转"
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
"id": row.get("id"),
|
"id": row.get("id"),
|
||||||
@@ -80,6 +84,8 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
"error": row.get("error") or "",
|
"error": row.get("error") or "",
|
||||||
"trading_usdt": trading_usdt,
|
"trading_usdt": trading_usdt,
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
|
"transfer_blocked": transfer_blocked,
|
||||||
|
"transfer_block_msg": transfer_block_msg,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|||||||
+123
-12
@@ -202,6 +202,8 @@
|
|||||||
return m === "sim" || m === "live" ? m : "";
|
return m === "sim" || m === "live" ? m : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var candidatesById = {};
|
||||||
|
|
||||||
function fmtTradingU(v) {
|
function fmtTradingU(v) {
|
||||||
if (v == null || v === "") return "—";
|
if (v == null || v === "") return "—";
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
@@ -232,6 +234,97 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchCandidateFunds(instanceId, tradingUsdt, fundingUsdt) {
|
||||||
|
const id = String(instanceId);
|
||||||
|
const it = candidatesById[id];
|
||||||
|
if (it) {
|
||||||
|
if (tradingUsdt != null) it.trading_usdt = tradingUsdt;
|
||||||
|
if (fundingUsdt != null) it.funding_usdt = fundingUsdt;
|
||||||
|
}
|
||||||
|
const row = document.querySelector(
|
||||||
|
'.cond-inst-row[data-inst-id="' + id.replace(/"/g, "") + '"]'
|
||||||
|
);
|
||||||
|
if (!row) return;
|
||||||
|
const tradeEl = row.querySelector(".cond-inst-funds--trading");
|
||||||
|
const fundEl = row.querySelector(".cond-inst-funds--funding");
|
||||||
|
if (tradeEl) tradeEl.textContent = "交易 " + fmtTradingU(tradingUsdt);
|
||||||
|
if (fundEl) fundEl.textContent = "资金 " + fmtTradingU(fundingUsdt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transferFundingToTrading(instanceId) {
|
||||||
|
const id = String(instanceId || "");
|
||||||
|
const it = candidatesById[id];
|
||||||
|
if (!it) return;
|
||||||
|
if (!it.online) {
|
||||||
|
window.alert("实例离线,无法划转");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (it.transfer_blocked) {
|
||||||
|
window.alert(it.transfer_block_msg || "当前不可划转");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fund = Number(it.funding_usdt);
|
||||||
|
const defAmt =
|
||||||
|
!Number.isNaN(fund) && fund > 0 ? String(Math.round(fund * 10000) / 10000) : "";
|
||||||
|
const raw = window.prompt("资金户 → 交易户(USDT)。本页划转,条件单表单不会丢失。", defAmt);
|
||||||
|
if (raw == null) return;
|
||||||
|
const amount = Number(String(raw).trim());
|
||||||
|
if (!(amount > 0)) {
|
||||||
|
window.alert("请输入大于 0 的数量");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const btn = document.querySelector(
|
||||||
|
'.cond-inst-xfer[data-inst-id="' + id.replace(/"/g, "") + '"]'
|
||||||
|
);
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "划转中…";
|
||||||
|
}
|
||||||
|
fetch("/api/instances/" + encodeURIComponent(id) + "/transfer", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: amount,
|
||||||
|
from: "funding",
|
||||||
|
to: "trading",
|
||||||
|
ccy: "USDT",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json().then(function (d) {
|
||||||
|
return { http: r.status, d: d };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (pack) {
|
||||||
|
const d = pack.d || {};
|
||||||
|
if (!d.ok) {
|
||||||
|
window.alert(d.msg || "划转失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchCandidateFunds(id, d.trading_usdt, d.funding_usdt);
|
||||||
|
const st = document.getElementById("cf-status");
|
||||||
|
if (st) {
|
||||||
|
st.textContent =
|
||||||
|
(it.name || id) +
|
||||||
|
" 已划入交易 " +
|
||||||
|
fmtTradingU(amount) +
|
||||||
|
"(交易 " +
|
||||||
|
fmtTradingU(d.trading_usdt) +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
window.alert("划转请求失败");
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = !!(it.transfer_blocked || !it.online);
|
||||||
|
btn.textContent = "划入交易";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderCandidateCard(it, checkedIds) {
|
function renderCandidateCard(it, checkedIds) {
|
||||||
const disabled = !it.selectable;
|
const disabled = !it.selectable;
|
||||||
const tip = disabled
|
const tip = disabled
|
||||||
@@ -245,13 +338,15 @@
|
|||||||
: "可选";
|
: "可选";
|
||||||
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 =
|
const xferBlocked = !!(it.transfer_blocked || !it.online);
|
||||||
it.funding_usdt != null
|
const xferTitle = xferBlocked
|
||||||
? "资金户 " + fmtTradingU(it.funding_usdt)
|
? it.transfer_block_msg || (!it.online ? "离线" : "不可划转")
|
||||||
: "";
|
: "资金户→交易户,本页完成不丢表单";
|
||||||
return (
|
return (
|
||||||
'<div class="cond-inst-row' +
|
'<div class="cond-inst-row' +
|
||||||
(disabled ? " is-off" : "") +
|
(disabled ? " is-off" : "") +
|
||||||
|
'" data-inst-id="' +
|
||||||
|
escapeHtml(id) +
|
||||||
'">' +
|
'">' +
|
||||||
'<label class="cond-inst' +
|
'<label class="cond-inst' +
|
||||||
(disabled ? " is-off" : "") +
|
(disabled ? " is-off" : "") +
|
||||||
@@ -270,15 +365,23 @@
|
|||||||
'<span class="cond-inst-tag">' +
|
'<span class="cond-inst-tag">' +
|
||||||
escapeHtml(tip) +
|
escapeHtml(tip) +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
'<span class="cond-inst-funds" title="' +
|
'<span class="cond-inst-funds cond-inst-funds--trading" title="交易账户可用">交易 ' +
|
||||||
escapeHtml(fundsTip || "交易账户可用") +
|
|
||||||
'">交易 ' +
|
|
||||||
escapeHtml(fmtTradingU(it.trading_usdt)) +
|
escapeHtml(fmtTradingU(it.trading_usdt)) +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
|
'<span class="cond-inst-funds cond-inst-funds--funding muted" title="资金账户可用">资金 ' +
|
||||||
|
escapeHtml(fmtTradingU(it.funding_usdt)) +
|
||||||
|
"</span>" +
|
||||||
"</label>" +
|
"</label>" +
|
||||||
'<button type="button" class="btn btn-sm cond-inst-open" data-inst-id="' +
|
'<button type="button" class="btn btn-sm cond-inst-xfer" data-inst-id="' +
|
||||||
encodeURIComponent(id) +
|
escapeHtml(id) +
|
||||||
'" title="新标签打开期权实例(可划转)">打开</button>' +
|
'" title="' +
|
||||||
|
escapeHtml(xferTitle) +
|
||||||
|
'"' +
|
||||||
|
(xferBlocked ? " disabled" : "") +
|
||||||
|
">划入交易</button>" +
|
||||||
|
'<button type="button" class="btn btn-secondary btn-sm cond-inst-open" data-inst-id="' +
|
||||||
|
escapeHtml(id) +
|
||||||
|
'" title="新标签打开实例">打开</button>' +
|
||||||
"</div>"
|
"</div>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -324,8 +427,10 @@
|
|||||||
const sim = [];
|
const sim = [];
|
||||||
const live = [];
|
const live = [];
|
||||||
const other = [];
|
const other = [];
|
||||||
|
candidatesById = {};
|
||||||
list.forEach(function (it) {
|
list.forEach(function (it) {
|
||||||
const m = instanceMode(it);
|
const m = instanceMode(it);
|
||||||
|
candidatesById[String(it.id)] = it;
|
||||||
if (m === "sim") sim.push(it);
|
if (m === "sim") sim.push(it);
|
||||||
else if (m === "live") live.push(it);
|
else if (m === "live") live.push(it);
|
||||||
else other.push(it);
|
else other.push(it);
|
||||||
@@ -334,12 +439,18 @@
|
|||||||
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-xfer").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
transferFundingToTrading(btn.getAttribute("data-inst-id") || "");
|
||||||
|
});
|
||||||
|
});
|
||||||
box.querySelectorAll(".cond-inst-open").forEach(function (btn) {
|
box.querySelectorAll(".cond-inst-open").forEach(function (btn) {
|
||||||
btn.addEventListener("click", function (ev) {
|
btn.addEventListener("click", function (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const iid = decodeURIComponent(btn.getAttribute("data-inst-id") || "");
|
openInstanceTab(btn.getAttribute("data-inst-id") || "", "options");
|
||||||
openInstanceTab(iid, "options");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1524,7 +1524,7 @@ 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(300px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.cond-inst-row {
|
.cond-inst-row {
|
||||||
@@ -1556,6 +1556,11 @@ input.readonly {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.cond-inst-funds--funding {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.cond-inst-xfer,
|
||||||
.cond-inst-open {
|
.cond-inst-open {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-self: center;
|
align-self: 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=47">
|
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=48">
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,7 +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>
|
<p class="muted" style="margin:0 0 8px;font-size:.76rem">展示交易/资金余额;「划入交易」在本页完成(资金→交易),表单不丢。「打开」仅备用进实例。</p>
|
||||||
<div id="cf-instances"></div>
|
<div id="cf-instances"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,6 +95,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='cond_orders.js') }}?v=8"></script>
|
<script src="{{ url_for('static', filename='cond_orders.js') }}?v=9"></script>
|
||||||
<script>HubCondOrders.boot();</script>
|
<script>HubCondOrders.boot();</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class TestCondCandidatesFunds(unittest.TestCase):
|
|||||||
"cond_order_locked": False,
|
"cond_order_locked": False,
|
||||||
"instance": {"mode": "live"},
|
"instance": {"mode": "live"},
|
||||||
"funds": {"trading_usdt": 123.456, "funding_usdt": 50.0},
|
"funds": {"trading_usdt": 123.456, "funding_usdt": 50.0},
|
||||||
|
"transfer_blocked": False,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,8 @@ class TestCondCandidatesFunds(unittest.TestCase):
|
|||||||
"cond_order_locked": False,
|
"cond_order_locked": False,
|
||||||
"instance": {"mode": "sim"},
|
"instance": {"mode": "sim"},
|
||||||
"funds": {},
|
"funds": {},
|
||||||
|
"transfer_blocked": True,
|
||||||
|
"transfer_block_msg": "期权持仓中禁止划转",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -39,8 +42,10 @@ class TestCondCandidatesFunds(unittest.TestCase):
|
|||||||
self.assertTrue(out[0]["selectable"])
|
self.assertTrue(out[0]["selectable"])
|
||||||
self.assertAlmostEqual(out[0]["trading_usdt"], 123.456, places=3)
|
self.assertAlmostEqual(out[0]["trading_usdt"], 123.456, places=3)
|
||||||
self.assertAlmostEqual(out[0]["funding_usdt"], 50.0, places=3)
|
self.assertAlmostEqual(out[0]["funding_usdt"], 50.0, places=3)
|
||||||
|
self.assertFalse(out[0]["transfer_blocked"])
|
||||||
self.assertFalse(out[1]["selectable"])
|
self.assertFalse(out[1]["selectable"])
|
||||||
self.assertIsNone(out[1]["trading_usdt"])
|
self.assertIsNone(out[1]["trading_usdt"])
|
||||||
|
self.assertTrue(out[1]["transfer_blocked"])
|
||||||
|
|
||||||
def test_open_url_tab_appended(self):
|
def test_open_url_tab_appended(self):
|
||||||
from lib.sso_lib import build_embed_open_url
|
from lib.sso_lib import build_embed_open_url
|
||||||
|
|||||||
Reference in New Issue
Block a user