diff --git a/static/cond_orders.js b/static/cond_orders.js index 702c068..9361ab4 100644 --- a/static/cond_orders.js +++ b/static/cond_orders.js @@ -1,333 +1,395 @@ -(function (global) { - function escapeHtml(s) { - return String(s) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); - } - - function leftTriggerSide(optType) { - return String(optType || "P").toUpperCase() === "C" ? "lte" : "gte"; - } - - function triggerHintText(optType, px) { - const put = String(optType || "P").toUpperCase() !== "C"; - const price = px && String(px).trim() !== "" ? String(px) : "触发价"; - if (put) { - return "看跌左侧单:指数上穿 " + price + " 后,在偏离区内开最近实值 Put。"; - } - return "看涨左侧单:指数下穿 " + price + " 后,在偏离区内开最近实值 Call。"; - } - - function syncTriggerHint() { - const hint = document.getElementById("cf-trigger-hint"); - const typeEl = document.getElementById("cf-opt-type"); - const pxEl = document.getElementById("cf-trigger-px"); - if (!hint || !typeEl) return; - hint.textContent = triggerHintText(typeEl.value, pxEl && pxEl.value); - hint.className = "cond-trigger-hint " + (typeEl.value === "C" ? "is-call" : "is-put"); - } - - function statusLabel(o) { - const st = o.status || ""; - const reason = o.end_reason || ""; - if (st === "done") { - const map = { - leave_up: "结束·上破", - leave_down: "结束·下破", - cancel: "已取消", - closed: "已完成", - }; - return map[reason] || "已结束"; - } - const map2 = { armed: "待命", triggering: "触发中", open: "持仓中" }; - return map2[st] || st || "—"; - } - - function statusClass(o) { - const st = o.status || ""; - if (st === "open") return "st-open"; - if (st === "triggering") return "st-trig"; - if (st === "armed") return "st-armed"; - return "st-done"; - } - - function instanceMode(it) { - const m = String((it && it.mode) || "").toLowerCase(); - return m === "sim" || m === "live" ? m : ""; - } - - function renderCandidateCard(it, checkedIds) { - const disabled = !it.selectable; - const tip = disabled - ? it.has_position - ? "已有持仓" - : it.cond_order_locked - ? "条件单占用" - : it.online - ? "不可选" - : "离线" - : "可选"; - const id = String(it.id); - const checked = !disabled && checkedIds.indexOf(id) >= 0; - return ( - '" - ); - } - - function renderCandidateGroup(mode, items, checkedIds) { - if (!items.length) return ""; - const label = - mode === "sim" || mode === "live" - ? '
' + - mode + - "
" - : ""; - return ( - '
' + - label + - '
' + - items - .map(function (it) { - return renderCandidateCard(it, checkedIds); - }) - .join("") + - "
" - ); - } - - async function loadCandidates() { - const box = document.getElementById("cf-instances"); - if (!box) return; - const checkedIds = Array.prototype.slice - .call(box.querySelectorAll('input[name="cf-inst"]:checked')) - .map(function (el) { - return decodeURIComponent(el.value || ""); - }); - const r = await fetch("/api/cond_orders/candidates"); - const d = await r.json(); - const list = d.instances || []; - if (!list.length) { - box.innerHTML = '暂无启用实例'; - return; - } - const sim = []; - const live = []; - const other = []; - list.forEach(function (it) { - const m = instanceMode(it); - if (m === "sim") sim.push(it); - else if (m === "live") live.push(it); - else other.push(it); - }); - box.innerHTML = - renderCandidateGroup("sim", sim, checkedIds) + - renderCandidateGroup("live", live, checkedIds) + - renderCandidateGroup("", other, checkedIds); - } - - async function loadOrders() { - const box = document.getElementById("cond-list"); - if (!box) return; - const r = await fetch("/api/cond_orders"); - const d = await r.json(); - const list = d.orders || []; - if (!list.length) { - box.innerHTML = '
暂无条件单
'; - return; - } - box.innerHTML = - '
' + - "" + - "" + - list - .map(function (o) { - const side = o.trigger_side === "lte" ? "下穿 ≤" : "上穿 ≥"; - const exit = - (o.profit_exit_enabled ? "翻倍×" + (o.profit_exit_mult || "") : "") + - (o.target_index != null ? " 目标" + o.target_index : ""); - const canCancel = o.status === "armed" || o.status === "triggering"; - const canExit = o.status === "open"; - let actions = ""; - if (canCancel) { - actions += - ''; - } - if (canExit) { - actions += - ''; - } - return ( - "" + - "" + - '" + - "" + - "" + - "" + - "" + - "" + - '" + - '" + - "" - ); - }) - .join("") + - "
客户端状态标的触发偏离到期h出场说明操作
" + - escapeHtml(o.instance_name || "") + - "' + - escapeHtml(statusLabel(o)) + - "" + - escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) + - "" + - escapeHtml(side + " " + (o.trigger_px != null ? o.trigger_px : "")) + - "" + - escapeHtml(o.deviation_pct != null ? o.deviation_pct + "%" : "—") + - "" + - escapeHtml(o.min_expiry_hours != null ? String(o.min_expiry_hours) : "—") + - "" + - escapeHtml(exit || "—") + - "' + - escapeHtml(o.message || "") + - "
' + - actions + - "
"; - - box.querySelectorAll("[data-cancel]").forEach(function (btn) { - btn.addEventListener("click", async function () { - if (!confirm("确认取消该条件单?")) return; - const oid = decodeURIComponent(btn.getAttribute("data-cancel") || ""); - const iid = decodeURIComponent(btn.getAttribute("data-iid") || ""); - const rr = await fetch("/api/cond_orders/" + encodeURIComponent(oid) + "/cancel", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ instance_id: Number(iid) }), - }); - const dd = await rr.json(); - alert(dd.msg || (dd.ok ? "已取消" : "失败")); - refresh(); - }); - }); - box.querySelectorAll("[data-exit]").forEach(function (btn) { - btn.addEventListener("click", async function () { - const oid = decodeURIComponent(btn.getAttribute("data-exit") || ""); - const iid = decodeURIComponent(btn.getAttribute("data-iid") || ""); - const curT = decodeURIComponent(btn.getAttribute("data-target") || ""); - const curM = decodeURIComponent(btn.getAttribute("data-mult") || "1"); - const pe = btn.getAttribute("data-pe") === "1"; - const t = prompt("目标指数(留空清除)", curT); - if (t === null) return; - const peOn = confirm("是否启用翻倍出场?\n当前:" + (pe ? "开" : "关")); - let mult = curM; - if (peOn) { - const m = prompt("翻倍倍数", curM); - if (m === null) return; - mult = m; - } - const rr = await fetch("/api/cond_orders/" + encodeURIComponent(oid) + "/exit", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - instance_id: Number(iid), - target_index: t === "" ? "" : t, - profit_exit_enabled: peOn, - profit_exit_mult: mult, - }), - }); - const dd = await rr.json(); - alert(dd.msg || (dd.ok ? "已更新" : "失败")); - refresh(); - }); - }); - } - - async function createOrder() { - const status = document.getElementById("cf-status"); - const ids = Array.prototype.slice - .call(document.querySelectorAll('input[name="cf-inst"]:checked')) - .map(function (el) { - return Number(decodeURIComponent(el.value)); - }); - if (!ids.length) { - alert("请选择至少一台可选客户端"); - return; - } - const optType = document.getElementById("cf-opt-type").value; - const body = { - instance_ids: ids, - underlying: document.getElementById("cf-underlying").value, - opt_type: optType, - trigger_side: leftTriggerSide(optType), - trigger_px: document.getElementById("cf-trigger-px").value, - deviation_pct: document.getElementById("cf-deviation").value, - min_expiry_hours: document.getElementById("cf-expiry-h").value, - target_index: document.getElementById("cf-target").value, - profit_exit_enabled: document.getElementById("cf-pe").checked, - profit_exit_mult: document.getElementById("cf-pe-mult").value, - }; - if (status) status.textContent = "下发中…"; - const r = await fetch("/api/cond_orders", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - const d = await r.json(); - if (status) status.textContent = d.msg || ""; - alert(d.msg || (d.ok ? "已下发" : "失败")); - refresh(); - } - - async function refresh() { - await Promise.all([loadCandidates(), loadOrders()]); - } - - function boot() { - const btn = document.getElementById("btn-cond-refresh"); - if (btn) btn.addEventListener("click", refresh); - const create = document.getElementById("btn-cond-create"); - if (create) create.addEventListener("click", createOrder); - const typeEl = document.getElementById("cf-opt-type"); - const pxEl = document.getElementById("cf-trigger-px"); - if (typeEl) typeEl.addEventListener("change", syncTriggerHint); - if (pxEl) pxEl.addEventListener("input", syncTriggerHint); - syncTriggerHint(); - refresh(); - setInterval(refresh, 10000); - } - - global.HubCondOrders = { boot: boot, refresh: refresh }; -})(window); +(function (global) { + function escapeHtml(s) { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + function leftTriggerSide(optType) { + return String(optType || "P").toUpperCase() === "C" ? "lte" : "gte"; + } + + function triggerHintText(optType, px) { + const put = String(optType || "P").toUpperCase() !== "C"; + const price = px && String(px).trim() !== "" ? String(px) : "触发价"; + if (put) { + return "看跌左侧单:指数上穿 " + price + " 后,在偏离区内开最近实值 Put。"; + } + return "看涨左侧单:指数下穿 " + price + " 后,在偏离区内开最近实值 Call。"; + } + + function syncTriggerHint() { + const hint = document.getElementById("cf-trigger-hint"); + const typeEl = document.getElementById("cf-opt-type"); + const pxEl = document.getElementById("cf-trigger-px"); + if (!hint || !typeEl) return; + hint.textContent = triggerHintText(typeEl.value, pxEl && pxEl.value); + hint.className = "cond-trigger-hint " + (typeEl.value === "C" ? "is-call" : "is-put"); + } + + function statusLabel(o) { + const st = o.status || ""; + const reason = o.end_reason || ""; + if (st === "done") { + const map = { + leave_up: "结束·上破", + leave_down: "结束·下破", + cancel: "已取消", + closed: "已完成", + }; + return map[reason] || "已结束"; + } + const map2 = { armed: "待命", triggering: "触发中", open: "持仓中" }; + return map2[st] || st || "—"; + } + + function statusClass(o) { + const st = o.status || ""; + if (st === "open") return "st-open"; + if (st === "triggering") return "st-trig"; + if (st === "armed") return "st-armed"; + return "st-done"; + } + + function isHubPhone() { + return ( + document.documentElement.classList.contains("hub-phone-html") || + !!(document.body && document.body.classList.contains("hub-phone")) + ); + } + + function orderTriggerText(o) { + const side = o.trigger_side === "lte" ? "下穿≤" : "上穿≥"; + const px = o.trigger_px != null ? String(o.trigger_px) : ""; + return side + px; + } + + function orderExitText(o) { + const bits = []; + if (o.profit_exit_enabled) bits.push("翻倍×" + (o.profit_exit_mult || "")); + if (o.target_index != null) bits.push("目标" + o.target_index); + return bits.join(" · "); + } + + function orderActionHtml(o) { + const canCancel = o.status === "armed" || o.status === "triggering"; + const canExit = o.status === "open"; + let actions = ""; + if (canCancel) { + actions += + ''; + } + if (canExit) { + actions += + ''; + } + return actions; + } + + function bindOrderActions(box) { + if (!box) return; + box.querySelectorAll("[data-cancel]").forEach(function (btn) { + btn.addEventListener("click", async function () { + if (!confirm("确认取消该条件单?")) return; + const oid = decodeURIComponent(btn.getAttribute("data-cancel") || ""); + const iid = decodeURIComponent(btn.getAttribute("data-iid") || ""); + const rr = await fetch("/api/cond_orders/" + encodeURIComponent(oid) + "/cancel", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ instance_id: Number(iid) }), + }); + const dd = await rr.json(); + alert(dd.msg || (dd.ok ? "已取消" : "失败")); + refresh(); + }); + }); + box.querySelectorAll("[data-exit]").forEach(function (btn) { + btn.addEventListener("click", async function () { + const oid = decodeURIComponent(btn.getAttribute("data-exit") || ""); + const iid = decodeURIComponent(btn.getAttribute("data-iid") || ""); + const curT = decodeURIComponent(btn.getAttribute("data-target") || ""); + const curM = decodeURIComponent(btn.getAttribute("data-mult") || "1"); + const pe = btn.getAttribute("data-pe") === "1"; + const t = prompt("目标指数(留空清除)", curT); + if (t === null) return; + const peOn = confirm("是否启用翻倍出场?\n当前:" + (pe ? "开" : "关")); + let mult = curM; + if (peOn) { + const m = prompt("翻倍倍数", curM); + if (m === null) return; + mult = m; + } + const rr = await fetch("/api/cond_orders/" + encodeURIComponent(oid) + "/exit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + instance_id: Number(iid), + target_index: t === "" ? "" : t, + profit_exit_enabled: peOn, + profit_exit_mult: mult, + }), + }); + const dd = await rr.json(); + alert(dd.msg || (dd.ok ? "已更新" : "失败")); + refresh(); + }); + }); + } + + function renderPhoneOrderCard(o) { + const exit = orderExitText(o); + const meta = []; + if (o.deviation_pct != null) meta.push("偏离 " + o.deviation_pct + "%"); + if (o.min_expiry_hours != null) meta.push("到期 " + o.min_expiry_hours + "h"); + if (exit) meta.push(exit); + const actions = orderActionHtml(o); + return ( + '
' + + '
' + + "" + + escapeHtml(o.instance_name || "") + + "" + + '' + + escapeHtml(statusLabel(o)) + + "
" + + '
' + + escapeHtml((o.underlying || "") + " " + (o.opt_type === "C" ? "Call" : "Put")) + + " · " + + escapeHtml(orderTriggerText(o)) + + "
" + + (meta.length ? '
' + escapeHtml(meta.join(" · ")) + "
" : "") + + (o.message ? '
' + escapeHtml(o.message) + "
" : "") + + (actions ? '
' + actions + "
" : "") + + "
" + ); + } + + function instanceMode(it) { + const m = String((it && it.mode) || "").toLowerCase(); + return m === "sim" || m === "live" ? m : ""; + } + + function renderCandidateCard(it, checkedIds) { + const disabled = !it.selectable; + const tip = disabled + ? it.has_position + ? "已有持仓" + : it.cond_order_locked + ? "条件单占用" + : it.online + ? "不可选" + : "离线" + : "可选"; + const id = String(it.id); + const checked = !disabled && checkedIds.indexOf(id) >= 0; + return ( + '" + ); + } + + function renderCandidateGroup(mode, items, checkedIds) { + if (!items.length) return ""; + const label = + mode === "sim" || mode === "live" + ? '
' + + mode + + "
" + : ""; + return ( + '
' + + label + + '
' + + items + .map(function (it) { + return renderCandidateCard(it, checkedIds); + }) + .join("") + + "
" + ); + } + + async function loadCandidates() { + const box = document.getElementById("cf-instances"); + if (!box) return; + const checkedIds = Array.prototype.slice + .call(box.querySelectorAll('input[name="cf-inst"]:checked')) + .map(function (el) { + return decodeURIComponent(el.value || ""); + }); + const r = await fetch("/api/cond_orders/candidates"); + const d = await r.json(); + const list = d.instances || []; + if (!list.length) { + box.innerHTML = '暂无启用实例'; + return; + } + const sim = []; + const live = []; + const other = []; + list.forEach(function (it) { + const m = instanceMode(it); + if (m === "sim") sim.push(it); + else if (m === "live") live.push(it); + else other.push(it); + }); + box.innerHTML = + renderCandidateGroup("sim", sim, checkedIds) + + renderCandidateGroup("live", live, checkedIds) + + renderCandidateGroup("", other, checkedIds); + } + + async function loadOrders() { + const box = document.getElementById("cond-list"); + if (!box) return; + const r = await fetch("/api/cond_orders"); + const d = await r.json(); + const list = d.orders || []; + if (!list.length) { + box.innerHTML = '
暂无条件单
'; + return; + } + if (isHubPhone()) { + box.innerHTML = + '
' + + list.map(renderPhoneOrderCard).join("") + + "
"; + bindOrderActions(box); + return; + } + box.innerHTML = + '
' + + "" + + "" + + list + .map(function (o) { + return ( + "" + + "" + + '" + + "" + + "" + + "" + + "" + + "" + + '" + + '" + + "" + ); + }) + .join("") + + "
客户端状态标的触发偏离到期h出场说明操作
" + + escapeHtml(o.instance_name || "") + + "' + + escapeHtml(statusLabel(o)) + + "" + + escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) + + "" + + escapeHtml(orderTriggerText(o).replace("≤", " ≤ ").replace("≥", " ≥ ")) + + "" + + escapeHtml(o.deviation_pct != null ? o.deviation_pct + "%" : "—") + + "" + + escapeHtml(o.min_expiry_hours != null ? String(o.min_expiry_hours) : "—") + + "" + + escapeHtml(orderExitText(o) || "—") + + "' + + escapeHtml(o.message || "") + + "
' + + orderActionHtml(o) + + "
"; + bindOrderActions(box); + } + + async function createOrder() { + const status = document.getElementById("cf-status"); + const ids = Array.prototype.slice + .call(document.querySelectorAll('input[name="cf-inst"]:checked')) + .map(function (el) { + return Number(decodeURIComponent(el.value)); + }); + if (!ids.length) { + alert("请选择至少一台可选客户端"); + return; + } + const optType = document.getElementById("cf-opt-type").value; + const body = { + instance_ids: ids, + underlying: document.getElementById("cf-underlying").value, + opt_type: optType, + trigger_side: leftTriggerSide(optType), + trigger_px: document.getElementById("cf-trigger-px").value, + deviation_pct: document.getElementById("cf-deviation").value, + min_expiry_hours: document.getElementById("cf-expiry-h").value, + target_index: document.getElementById("cf-target").value, + profit_exit_enabled: document.getElementById("cf-pe").checked, + profit_exit_mult: document.getElementById("cf-pe-mult").value, + }; + if (status) status.textContent = "下发中…"; + const r = await fetch("/api/cond_orders", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const d = await r.json(); + if (status) status.textContent = d.msg || ""; + alert(d.msg || (d.ok ? "已下发" : "失败")); + refresh(); + } + + async function refresh() { + await Promise.all([loadCandidates(), loadOrders()]); + } + + function boot() { + const btn = document.getElementById("btn-cond-refresh"); + if (btn) btn.addEventListener("click", refresh); + const create = document.getElementById("btn-cond-create"); + if (create) create.addEventListener("click", createOrder); + const typeEl = document.getElementById("cf-opt-type"); + const pxEl = document.getElementById("cf-trigger-px"); + if (typeEl) typeEl.addEventListener("change", syncTriggerHint); + if (pxEl) pxEl.addEventListener("input", syncTriggerHint); + syncTriggerHint(); + refresh(); + setInterval(refresh, 10000); + } + + global.HubCondOrders = { boot: boot, refresh: refresh }; +})(window); diff --git a/static/hub.css b/static/hub.css index 5fe72b6..b2db34b 100644 --- a/static/hub.css +++ b/static/hub.css @@ -1979,6 +1979,80 @@ html.hub-phone-html .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; } +html.hub-phone-html body.page-cond-orders .cond-lead { + display: none; +} +html.hub-phone-html body.page-cond-orders .cond-layout { + display: flex; + flex-direction: column; + gap: 14px; +} +html.hub-phone-html body.page-cond-orders .cond-layout > .cond-card:last-child { + order: -1; +} +html.hub-phone-html body.page-cond-orders .cond-card { + padding: 14px; + border-radius: 14px; +} +html.hub-phone-html body.page-cond-orders .cond-card-head { + margin-bottom: 10px; +} +html.hub-phone-html body.page-cond-orders .cond-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 12px; +} +html.hub-phone-html body.page-cond-orders .cond-switch { + min-height: 0; + grid-column: 1 / -1; +} +html.hub-phone-html body.page-cond-orders .cond-inst-grid { + grid-template-columns: 1fr; +} +html.hub-phone-html body.page-cond-orders .cond-actions { + flex-direction: column; + align-items: stretch; +} +html.hub-phone-html body.page-cond-orders .cond-actions .btn { + width: 100%; +} +.hub-phone-cond-list { + display: flex; + flex-direction: column; + gap: 10px; +} +.hub-phone-cond { + padding: 12px 12px 10px; + border-radius: 12px; + border: 1px solid rgba(56, 189, 248, 0.18); + background: rgba(8, 16, 30, 0.72); +} +.hub-phone-cond-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; +} +.hub-phone-cond-head strong { + font-size: 0.95rem; +} +.hub-phone-cond-main { + font-size: 0.98rem; + font-weight: 600; + line-height: 1.4; +} +.hub-phone-cond-meta, +.hub-phone-cond-msg { + margin-top: 4px; + font-size: 0.78rem; + line-height: 1.4; +} +.hub-phone-cond-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} html.hub-phone-html .login-wrap { padding: 48px 16px 24px; } diff --git a/templates/base.html b/templates/base.html index 18ae60e..a8d9ca1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,7 +11,7 @@ - + - -{% endblock %} +{% extends "base.html" %} +{% block title %}条件单 · 期权中控{% endblock %} +{% block body %} +
+
+

左侧条件开仓

+

中控只下发;盯盘、买币、开平仓都在客户端。有仓或已占用的机器不可选。

+
+ +
+ +
+
+
+

新建条件单

+ 左侧单 +
+ +
+
入场
+
+ + + + + +
+
+
+ +
+
出场
+
+ + + +
+
+ +
+
客户端
+
+
+ +
+ + +
+
+ +
+
+

条件单列表

+
+
加载中…
+
+
+{% endblock %} +{% block scripts %} + + +{% endblock %}