总览支持向客户端下发买一全平:确认后转发,无效买一拒绝。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,7 +25,7 @@ if str(ROOT) not in sys.path:
|
||||
ENV_PATH = ROOT / ".env"
|
||||
load_dotenv(ENV_PATH)
|
||||
|
||||
from lib.bridge_client_lib import pull_all_enabled
|
||||
from lib.bridge_client_lib import post_close_full, pull_all_enabled
|
||||
from lib.db_lib import init_db
|
||||
from lib.env_lib import apply_env_updates
|
||||
from lib.instances_lib import (
|
||||
@@ -241,5 +241,28 @@ def api_open_url(iid: int):
|
||||
return jsonify({"ok": True, "url": url, "base_url": inst["base_url"]})
|
||||
|
||||
|
||||
@app.route("/api/instances/<int:iid>/options/close", methods=["POST"])
|
||||
@login_required
|
||||
def api_instance_options_close(iid: int):
|
||||
"""总览下发全平:中控只转发,客户端本地买一限价执行."""
|
||||
inst = get_instance(iid)
|
||||
if not inst:
|
||||
return jsonify({"ok": False, "msg": "实例不存在"}), 404
|
||||
if not inst.get("enabled"):
|
||||
return jsonify({"ok": False, "msg": "实例未启用"}), 400
|
||||
data = request.get_json(silent=True) or {}
|
||||
opt_inst = (data.get("inst_id") or "").strip()
|
||||
if not opt_inst:
|
||||
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
||||
result = post_close_full(inst, inst_id=opt_inst)
|
||||
status = 200
|
||||
if not result.get("ok"):
|
||||
# 客户端业务拒绝(残档等)仍 200 带 ok:false;仅传输层失败用 502
|
||||
msg = str(result.get("msg") or "")
|
||||
if msg.startswith("HTTP ") or "超时" in msg or "下发失败" in msg or "缺少 URL" in msg:
|
||||
status = 502
|
||||
return jsonify(result), status
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host=APP_HOST, port=APP_PORT, debug=True, threaded=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""拉取各 crypto_okx 客户端只读快照."""
|
||||
"""拉取各 crypto_okx 客户端快照;以及受限写指令(买一全平)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -18,6 +18,13 @@ def _timeout() -> float:
|
||||
return 5.0
|
||||
|
||||
|
||||
def _close_timeout() -> float:
|
||||
try:
|
||||
return max(8.0, float(os.getenv("HUB_CLOSE_TIMEOUT_SEC") or 45))
|
||||
except (TypeError, ValueError):
|
||||
return 45.0
|
||||
|
||||
|
||||
def fetch_one_snapshot(inst: dict[str, Any]) -> dict[str, Any]:
|
||||
base = (inst.get("base_url") or "").rstrip("/")
|
||||
token = (inst.get("bridge_token") or "").strip()
|
||||
@@ -62,6 +69,44 @@ def fetch_one_snapshot(inst: dict[str, Any]) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def post_close_full(inst: dict[str, Any], *, inst_id: str) -> dict[str, Any]:
|
||||
"""向客户端下发买一全平指令;下单在客户端本地执行."""
|
||||
base = (inst.get("base_url") or "").rstrip("/")
|
||||
token = (inst.get("bridge_token") or "").strip()
|
||||
iid = (inst_id or "").strip()
|
||||
if not base or not token:
|
||||
return {"ok": False, "msg": "缺少 URL 或 Token"}
|
||||
if not iid:
|
||||
return {"ok": False, "msg": "缺少 inst_id"}
|
||||
url = f"{base}/api/hub/options/close"
|
||||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
headers={
|
||||
"X-Hub-Token": token,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"inst_id": iid},
|
||||
timeout=_close_timeout(),
|
||||
)
|
||||
try:
|
||||
data = r.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
if isinstance(data, dict) and data:
|
||||
if r.status_code >= 400 and "ok" not in data:
|
||||
data = dict(data)
|
||||
data["ok"] = False
|
||||
data.setdefault("msg", f"HTTP {r.status_code}")
|
||||
return data
|
||||
return {"ok": False, "msg": f"HTTP {r.status_code}"}
|
||||
except requests.Timeout:
|
||||
return {"ok": False, "msg": "客户端超时(下单可能仍在进行,请刷新确认)"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e) or "下发失败"}
|
||||
|
||||
|
||||
def pull_all_enabled() -> list[dict[str, Any]]:
|
||||
instances = list_instances(enabled_only=True)
|
||||
if not instances:
|
||||
|
||||
+19
-1
@@ -196,6 +196,18 @@ button, .btn {
|
||||
}
|
||||
.btn:hover, button:hover { filter: brightness(1.06); }
|
||||
.btn-danger { background: #9f1239; color: #fff; }
|
||||
.btn-sm {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn[disabled] {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
filter: none;
|
||||
}
|
||||
.pos-brief-main { min-width: 0; }
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
@@ -252,7 +264,13 @@ button, .btn {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.pos-brief li { margin: 2px 0; }
|
||||
.pos-brief li {
|
||||
margin: 2px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
|
||||
+83
-4
@@ -24,13 +24,41 @@
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function positionSummary(brief) {
|
||||
function positionSummary(brief, instanceId, online) {
|
||||
const list = brief || [];
|
||||
if (!list.length) return '<span class="muted">暂无持仓</span>';
|
||||
const items = list.slice(0, 8).map(function (p) {
|
||||
const id = escapeHtml(p.inst_id || "");
|
||||
const id = p.inst_id || "";
|
||||
const sheets = escapeHtml(String(p.sheets != null ? p.sheets : "—"));
|
||||
return "<li>" + id + " · " + sheets + "张 · " + fmtPnlHtml(p.pnl_usdt) + "</li>";
|
||||
const invalid = !!(p.bid_invalid || (p.bid_px == null && (p.bid_sz == null || p.bid_sz === "")));
|
||||
const reason = (p.bid_invalid_reason || "买一无效,禁止平仓").toString();
|
||||
let closeBtn = "";
|
||||
if (online && id) {
|
||||
if (invalid) {
|
||||
closeBtn =
|
||||
'<button type="button" class="btn btn-sm btn-danger" disabled title="' +
|
||||
escapeHtml(reason) +
|
||||
'">平仓</button>';
|
||||
} else {
|
||||
closeBtn =
|
||||
'<button type="button" class="btn btn-sm btn-danger" data-close-inst="' +
|
||||
encodeURIComponent(String(instanceId)) +
|
||||
'" data-close-opt="' +
|
||||
encodeURIComponent(id) +
|
||||
'" title="买一限价全平">平仓</button>';
|
||||
}
|
||||
}
|
||||
return (
|
||||
"<li><span class=\"pos-brief-main\">" +
|
||||
escapeHtml(id) +
|
||||
" · " +
|
||||
sheets +
|
||||
"张 · " +
|
||||
fmtPnlHtml(p.pnl_usdt) +
|
||||
"</span>" +
|
||||
closeBtn +
|
||||
"</li>"
|
||||
);
|
||||
});
|
||||
const more =
|
||||
list.length > 8
|
||||
@@ -120,7 +148,7 @@
|
||||
fmtPnlHtml(pnl) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
positionSummary(brief) +
|
||||
positionSummary(brief, row.id, online) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
bidLiquiditySummary(brief) +
|
||||
@@ -140,6 +168,52 @@
|
||||
);
|
||||
}
|
||||
|
||||
async function requestClose(btn) {
|
||||
const instanceId = decodeURIComponent(btn.getAttribute("data-close-inst") || "");
|
||||
const optId = decodeURIComponent(btn.getAttribute("data-close-opt") || "");
|
||||
if (!instanceId || !optId) return;
|
||||
if (
|
||||
!confirm(
|
||||
"确认向该客户端下发买一限价全平?\n\n合约: " +
|
||||
optId +
|
||||
"\n\n无效买一将被拒绝;下单在客户端本地执行。"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const oldText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = "平仓中…";
|
||||
try {
|
||||
const rr = await fetch("/api/instances/" + encodeURIComponent(instanceId) + "/options/close", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify({ inst_id: optId }),
|
||||
});
|
||||
let dd = {};
|
||||
try {
|
||||
dd = await rr.json();
|
||||
} catch (e) {
|
||||
dd = {};
|
||||
}
|
||||
if (dd && dd.ok) {
|
||||
let msg = dd.msg || "已提交买一全平";
|
||||
if (dd.submitted_sheets != null) msg += " · " + dd.submitted_sheets + " 张";
|
||||
if (dd.fully_closed) msg += " · 已全平";
|
||||
alert(msg);
|
||||
await refresh();
|
||||
} else {
|
||||
alert((dd && dd.msg) || "平仓失败");
|
||||
btn.disabled = false;
|
||||
btn.textContent = oldText;
|
||||
}
|
||||
} catch (e) {
|
||||
alert((e && e.message) || "请求失败");
|
||||
btn.disabled = false;
|
||||
btn.textContent = oldText;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const status = document.getElementById("overview-status");
|
||||
const grid = document.getElementById("overview-grid");
|
||||
@@ -170,6 +244,11 @@
|
||||
else alert(dd.msg || "无法打开");
|
||||
});
|
||||
});
|
||||
grid.querySelectorAll("[data-close-opt]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
requestClose(btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (status) status.textContent = "已更新 · 共 " + list.length + " 台";
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user