diff --git a/app.py b/app.py index 3f02bb1..b9e410e 100644 --- a/app.py +++ b/app.py @@ -356,11 +356,16 @@ def api_cond_orders_create(): return jsonify({"ok": False, "msg": "没有可下发的客户端", "rejected": rejected}) hub_order_id = (data.get("hub_order_id") or "").strip() or str(uuid.uuid4()) + opt_type = (data.get("opt_type") or "P").strip().upper() + if opt_type not in ("P", "C"): + opt_type = "P" + # 左侧单:看跌上穿、看涨下穿 + trigger_side = "gte" if opt_type == "P" else "lte" payload = { "hub_order_id": hub_order_id, "underlying": (data.get("underlying") or "ETH").strip().upper(), - "opt_type": (data.get("opt_type") or "P").strip().upper(), - "trigger_side": (data.get("trigger_side") or "gte").strip().lower(), + "opt_type": opt_type, + "trigger_side": trigger_side, "trigger_px": data.get("trigger_px"), "deviation_pct": data.get("deviation_pct"), "min_expiry_hours": data.get("min_expiry_hours"), diff --git a/static/cond_orders.js b/static/cond_orders.js index 595745e..46b6ac4 100644 --- a/static/cond_orders.js +++ b/static/cond_orders.js @@ -7,6 +7,28 @@ .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 || ""; @@ -23,6 +45,14 @@ 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"; + } + async function loadCandidates() { const box = document.getElementById("cf-instances"); if (!box) return; @@ -46,18 +76,22 @@ : "离线" : "可选"; return ( - '" + "" ); }) .join(""); @@ -70,16 +104,16 @@ const d = await r.json(); const list = d.orders || []; if (!list.length) { - box.innerHTML = '暂无条件单'; + box.innerHTML = '
| 客户端 | 状态 | 标的 | 触发 | 偏离 | 到期h | 出场 | 说明 | 操作 | " + "" + escapeHtml(o.instance_name || "") + " | " + - "" + + ' | ' + escapeHtml(statusLabel(o)) + - " | " + + "" + "" + escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) + " | " + @@ -131,7 +167,7 @@ "" + escapeHtml(exit || "—") + " | " + - "" + + ' | ' + escapeHtml(o.message || "") + " | " + '' +
@@ -202,11 +238,12 @@
alert("请选择至少一台可选客户端");
return;
}
+ const optType = document.getElementById("cf-opt-type").value;
const body = {
instance_ids: ids,
underlying: document.getElementById("cf-underlying").value,
- opt_type: document.getElementById("cf-opt-type").value,
- trigger_side: document.getElementById("cf-trigger-side").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,
@@ -235,6 +272,11 @@
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);
}
diff --git a/static/hub.css b/static/hub.css
index d4f77e9..3b46f73 100644
--- a/static/hub.css
+++ b/static/hub.css
@@ -175,7 +175,7 @@ body.page-login .main {
}
.stack label, .form-grid label { display: block; margin: 8px 0; }
-input[type="text"], input[type="password"], input:not([type]) {
+input[type="text"], input[type="password"], input[type="number"], input:not([type]), select {
width: 100%;
padding: 8px 10px;
border-radius: 8px;
@@ -431,3 +431,115 @@ input.readonly {
padding-right: clamp(32px, 6vw, 96px);
}
}
+
+.cond-head { justify-content: space-between; align-items: flex-start; }
+.cond-lead { margin: 6px 0 0; max-width: 720px; line-height: 1.5; }
+.cond-layout { display: grid; gap: 16px; }
+.cond-card { padding: 18px 18px 16px; }
+.cond-card-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 14px;
+}
+.cond-card-head h2 {
+ margin: 0;
+ font-size: 1.02rem;
+ letter-spacing: 0.03em;
+}
+.cond-chip {
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ color: #041018;
+ background: linear-gradient(90deg, var(--accent-deep), var(--accent));
+ border-radius: 999px;
+ padding: 3px 10px;
+}
+.cond-section {
+ border-top: 1px solid rgba(56, 189, 248, 0.12);
+ padding: 14px 0 4px;
+}
+.cond-section:first-of-type { border-top: 0; padding-top: 0; }
+.cond-section-title {
+ font-size: 0.78rem;
+ color: var(--accent-2);
+ letter-spacing: 0.08em;
+ margin-bottom: 10px;
+ font-weight: 600;
+}
+.cond-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 12px 14px;
+ align-items: end;
+}
+.cond-grid label {
+ margin: 0;
+ font-size: 0.8rem;
+ color: #b6c7da;
+}
+.cond-grid input,
+.cond-grid select {
+ margin-top: 6px;
+}
+.cond-trigger-hint {
+ grid-column: 1 / -1;
+ padding: 9px 12px;
+ border-radius: 10px;
+ font-size: 0.82rem;
+ line-height: 1.45;
+ border: 1px solid var(--line);
+ background: rgba(8, 16, 30, 0.7);
+ color: var(--text);
+}
+.cond-trigger-hint.is-put { border-color: rgba(251, 113, 133, 0.35); }
+.cond-trigger-hint.is-call { border-color: rgba(52, 211, 153, 0.35); }
+.cond-switch {
+ display: flex !important;
+ align-items: center;
+ gap: 8px;
+ min-height: 62px;
+}
+.cond-switch input { width: auto; margin: 0; }
+.cond-inst-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 10px;
+}
+.cond-inst {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 1px solid var(--line);
+ background: rgba(8, 16, 30, 0.72);
+ 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-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 14px;
+}
+.cond-empty { padding: 28px 8px; text-align: center; }
+.cond-st {
+ display: inline-block;
+ min-width: 64px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+.cond-st.st-armed { color: var(--accent-2); background: rgba(56, 189, 248, 0.12); }
+.cond-st.st-trig { color: #fbbf24; background: rgba(251, 191, 36, 0.12); }
+.cond-st.st-open { color: var(--ok); background: rgba(52, 211, 153, 0.12); }
+.cond-st.st-done { color: var(--muted); background: rgba(138, 160, 184, 0.12); }
+.cond-table-wrap { margin-top: 0; }
diff --git a/templates/cond_orders.html b/templates/cond_orders.html
index 73835f6..7f392ff 100644
--- a/templates/cond_orders.html
+++ b/templates/cond_orders.html
@@ -1,63 +1,82 @@
{% extends "base.html" %}
{% block title %}条件单 · 期权中控{% endblock %}
{% block body %}
- 条件开仓单+
+
左侧条件开仓+中控只下发;盯盘、买币、开平仓都在客户端。有仓或已占用的机器不可选。 +中控只下发指令;行情监控与买币/开仓/平仓/卖币均在客户端本地执行。有仓或已占用的机器不可选。 -
-
+新建条件单-
-
-
-
-
-
-
-
-
-
-
-
-
- 选择客户端
-
-
-
-
-
-
+
+
-新建条件单+ 左侧单 +
-
{% endblock %}
{% block scripts %}
条件单列表-加载中…
+
+
+
+ 入场
+
+
+
+
+
+
+
+
+
+
+
+ 出场
+
+
+
+
+
+
+
+
+ 客户端
+
+
+
+
+
+
+
+
+
+ 条件单列表+加载中…
+ |
|---|