中控条件单页:下发/取消/改出场,有仓客户端不可选。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,7 +25,14 @@ if str(ROOT) not in sys.path:
|
|||||||
ENV_PATH = ROOT / ".env"
|
ENV_PATH = ROOT / ".env"
|
||||||
load_dotenv(ENV_PATH)
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
from lib.bridge_client_lib import post_close_full, pull_all_enabled
|
from lib.bridge_client_lib import (
|
||||||
|
dispatch_cond_order_to_ids,
|
||||||
|
fetch_cond_orders,
|
||||||
|
post_close_full,
|
||||||
|
post_cond_cancel,
|
||||||
|
post_cond_exit,
|
||||||
|
pull_all_enabled,
|
||||||
|
)
|
||||||
from lib.db_lib import init_db
|
from lib.db_lib import init_db
|
||||||
from lib.env_lib import apply_env_updates
|
from lib.env_lib import apply_env_updates
|
||||||
from lib.instances_lib import (
|
from lib.instances_lib import (
|
||||||
@@ -264,5 +271,148 @@ def api_instance_options_close(iid: int):
|
|||||||
return jsonify(result), status
|
return jsonify(result), status
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/cond_orders")
|
||||||
|
@login_required
|
||||||
|
def cond_orders_page():
|
||||||
|
return render_template("cond_orders.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/cond_orders/candidates")
|
||||||
|
@login_required
|
||||||
|
def api_cond_order_candidates():
|
||||||
|
"""可选客户端:在线且无持仓、未被条件单锁定."""
|
||||||
|
rows = pull_all_enabled()
|
||||||
|
out = []
|
||||||
|
for row in rows:
|
||||||
|
snap = row.get("snapshot") if isinstance(row.get("snapshot"), dict) else {}
|
||||||
|
brief = snap.get("positions_brief") or snap.get("positions") or []
|
||||||
|
has_pos = isinstance(brief, list) and len(brief) > 0
|
||||||
|
locked = bool(snap.get("cond_order_locked"))
|
||||||
|
selectable = bool(row.get("online")) and not has_pos and not locked
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"id": row.get("id"),
|
||||||
|
"name": row.get("name"),
|
||||||
|
"online": bool(row.get("online")),
|
||||||
|
"has_position": has_pos,
|
||||||
|
"cond_order_locked": locked,
|
||||||
|
"selectable": selectable,
|
||||||
|
"error": row.get("error") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True, "instances": out})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/cond_orders", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def api_cond_orders_list():
|
||||||
|
instances = list_instances(enabled_only=True)
|
||||||
|
all_orders = []
|
||||||
|
for inst in instances:
|
||||||
|
data = fetch_cond_orders(inst)
|
||||||
|
for o in data.get("orders") or []:
|
||||||
|
if not isinstance(o, dict):
|
||||||
|
continue
|
||||||
|
item = dict(o)
|
||||||
|
item["instance_id"] = inst.get("id")
|
||||||
|
item["instance_name"] = inst.get("name")
|
||||||
|
all_orders.append(item)
|
||||||
|
return jsonify({"ok": True, "orders": all_orders})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/cond_orders", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_cond_orders_create():
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
ids_raw = data.get("instance_ids") or []
|
||||||
|
try:
|
||||||
|
instance_ids = [int(x) for x in ids_raw]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({"ok": False, "msg": "instance_ids 无效"})
|
||||||
|
if not instance_ids:
|
||||||
|
return jsonify({"ok": False, "msg": "请选择客户端"})
|
||||||
|
|
||||||
|
# 再次按快照过滤有仓/锁定
|
||||||
|
rows = {r.get("id"): r for r in pull_all_enabled()}
|
||||||
|
allowed = []
|
||||||
|
rejected = []
|
||||||
|
for iid in instance_ids:
|
||||||
|
row = rows.get(iid)
|
||||||
|
if not row or not row.get("online"):
|
||||||
|
rejected.append({"instance_id": iid, "msg": "离线或不可用"})
|
||||||
|
continue
|
||||||
|
snap = row.get("snapshot") if isinstance(row.get("snapshot"), dict) else {}
|
||||||
|
brief = snap.get("positions_brief") or []
|
||||||
|
if brief:
|
||||||
|
rejected.append({"instance_id": iid, "msg": "已有持仓不可选"})
|
||||||
|
continue
|
||||||
|
if snap.get("cond_order_locked"):
|
||||||
|
rejected.append({"instance_id": iid, "msg": "已有条件单占用"})
|
||||||
|
continue
|
||||||
|
allowed.append(iid)
|
||||||
|
if not allowed:
|
||||||
|
return jsonify({"ok": False, "msg": "没有可下发的客户端", "rejected": rejected})
|
||||||
|
|
||||||
|
hub_order_id = (data.get("hub_order_id") or "").strip() or str(uuid.uuid4())
|
||||||
|
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(),
|
||||||
|
"trigger_px": data.get("trigger_px"),
|
||||||
|
"deviation_pct": data.get("deviation_pct"),
|
||||||
|
"min_expiry_hours": data.get("min_expiry_hours"),
|
||||||
|
"exit": {
|
||||||
|
"target_index": data.get("target_index"),
|
||||||
|
"profit_exit_enabled": bool(data.get("profit_exit_enabled")),
|
||||||
|
"profit_exit_mult": data.get("profit_exit_mult"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
results = dispatch_cond_order_to_ids(allowed, payload)
|
||||||
|
ok_n = sum(1 for r in results if r.get("ok"))
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": ok_n > 0,
|
||||||
|
"msg": f"成功 {ok_n}/{len(results)}",
|
||||||
|
"hub_order_id": hub_order_id,
|
||||||
|
"results": results,
|
||||||
|
"rejected": rejected,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/cond_orders/<hub_order_id>/cancel", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_cond_orders_cancel(hub_order_id: str):
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
iid = data.get("instance_id")
|
||||||
|
if iid is None:
|
||||||
|
return jsonify({"ok": False, "msg": "缺少 instance_id"})
|
||||||
|
inst = get_instance(int(iid))
|
||||||
|
if not inst:
|
||||||
|
return jsonify({"ok": False, "msg": "实例不存在"}), 404
|
||||||
|
return jsonify(post_cond_cancel(inst, hub_order_id))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/cond_orders/<hub_order_id>/exit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_cond_orders_exit(hub_order_id: str):
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
iid = data.get("instance_id")
|
||||||
|
if iid is None:
|
||||||
|
return jsonify({"ok": False, "msg": "缺少 instance_id"})
|
||||||
|
inst = get_instance(int(iid))
|
||||||
|
if not inst:
|
||||||
|
return jsonify({"ok": False, "msg": "实例不存在"}), 404
|
||||||
|
payload = {
|
||||||
|
"target_index": data.get("target_index"),
|
||||||
|
"profit_exit_enabled": data.get("profit_exit_enabled"),
|
||||||
|
"profit_exit_mult": data.get("profit_exit_mult"),
|
||||||
|
}
|
||||||
|
return jsonify(post_cond_exit(inst, hub_order_id, payload))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host=APP_HOST, port=APP_PORT, debug=True, threaded=True)
|
app.run(host=APP_HOST, port=APP_PORT, debug=True, threaded=True)
|
||||||
|
|||||||
+114
-7
@@ -1,4 +1,4 @@
|
|||||||
"""拉取各 crypto_okx 客户端快照;以及受限写指令(买一全平)."""
|
"""拉取各 crypto_okx 客户端快照;以及受限写指令(买一全平/条件单)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -8,7 +8,7 @@ from typing import Any
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from lib.instances_lib import list_instances
|
from lib.instances_lib import get_instance, list_instances
|
||||||
|
|
||||||
|
|
||||||
def _timeout() -> float:
|
def _timeout() -> float:
|
||||||
@@ -25,6 +25,14 @@ def _close_timeout() -> float:
|
|||||||
return 45.0
|
return 45.0
|
||||||
|
|
||||||
|
|
||||||
|
def _headers(token: str) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"X-Hub-Token": token,
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def fetch_one_snapshot(inst: dict[str, Any]) -> dict[str, Any]:
|
def fetch_one_snapshot(inst: dict[str, Any]) -> dict[str, Any]:
|
||||||
base = (inst.get("base_url") or "").rstrip("/")
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
token = (inst.get("bridge_token") or "").strip()
|
token = (inst.get("bridge_token") or "").strip()
|
||||||
@@ -82,11 +90,7 @@ def post_close_full(inst: dict[str, Any], *, inst_id: str) -> dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url,
|
url,
|
||||||
headers={
|
headers=_headers(token),
|
||||||
"X-Hub-Token": token,
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
json={"inst_id": iid},
|
json={"inst_id": iid},
|
||||||
timeout=_close_timeout(),
|
timeout=_close_timeout(),
|
||||||
)
|
)
|
||||||
@@ -107,6 +111,88 @@ def post_close_full(inst: dict[str, Any], *, inst_id: str) -> dict[str, Any]:
|
|||||||
return {"ok": False, "msg": str(e) or "下发失败"}
|
return {"ok": False, "msg": str(e) or "下发失败"}
|
||||||
|
|
||||||
|
|
||||||
|
def post_cond_order(inst: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
|
token = (inst.get("bridge_token") or "").strip()
|
||||||
|
if not base or not token:
|
||||||
|
return {"ok": False, "msg": "缺少 URL 或 Token", "instance_id": inst.get("id")}
|
||||||
|
url = f"{base}/api/hub/options/cond_orders"
|
||||||
|
try:
|
||||||
|
r = requests.post(url, headers=_headers(token), json=payload, timeout=_close_timeout())
|
||||||
|
data = r.json() if r.content else {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = {"ok": False, "msg": f"HTTP {r.status_code}"}
|
||||||
|
data["instance_id"] = inst.get("id")
|
||||||
|
data["instance_name"] = inst.get("name")
|
||||||
|
return data
|
||||||
|
except requests.Timeout:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": "客户端超时",
|
||||||
|
"instance_id": inst.get("id"),
|
||||||
|
"instance_name": inst.get("name"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": str(e) or "下发失败",
|
||||||
|
"instance_id": inst.get("id"),
|
||||||
|
"instance_name": inst.get("name"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def post_cond_cancel(inst: dict[str, Any], hub_order_id: str) -> dict[str, Any]:
|
||||||
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
|
token = (inst.get("bridge_token") or "").strip()
|
||||||
|
oid = (hub_order_id or "").strip()
|
||||||
|
if not base or not token or not oid:
|
||||||
|
return {"ok": False, "msg": "参数不完整"}
|
||||||
|
url = f"{base}/api/hub/options/cond_orders/{oid}/cancel"
|
||||||
|
try:
|
||||||
|
r = requests.post(url, headers=_headers(token), json={}, timeout=_timeout())
|
||||||
|
data = r.json() if r.content else {}
|
||||||
|
return data if isinstance(data, dict) else {"ok": False, "msg": f"HTTP {r.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "msg": str(e) or "取消失败"}
|
||||||
|
|
||||||
|
|
||||||
|
def post_cond_exit(inst: dict[str, Any], hub_order_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
|
token = (inst.get("bridge_token") or "").strip()
|
||||||
|
oid = (hub_order_id or "").strip()
|
||||||
|
if not base or not token or not oid:
|
||||||
|
return {"ok": False, "msg": "参数不完整"}
|
||||||
|
url = f"{base}/api/hub/options/cond_orders/{oid}/exit"
|
||||||
|
try:
|
||||||
|
r = requests.post(url, headers=_headers(token), json=payload or {}, timeout=_timeout())
|
||||||
|
data = r.json() if r.content else {}
|
||||||
|
return data if isinstance(data, dict) else {"ok": False, "msg": f"HTTP {r.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "msg": str(e) or "更新失败"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_cond_orders(inst: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
base = (inst.get("base_url") or "").rstrip("/")
|
||||||
|
token = (inst.get("bridge_token") or "").strip()
|
||||||
|
if not base or not token:
|
||||||
|
return {"ok": False, "msg": "缺少 URL 或 Token", "orders": []}
|
||||||
|
url = f"{base}/api/hub/options/cond_orders"
|
||||||
|
try:
|
||||||
|
r = requests.get(
|
||||||
|
url,
|
||||||
|
headers={"X-Hub-Token": token, "Accept": "application/json"},
|
||||||
|
timeout=_timeout(),
|
||||||
|
)
|
||||||
|
data = r.json() if r.content else {}
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data["instance_id"] = inst.get("id")
|
||||||
|
data["instance_name"] = inst.get("name")
|
||||||
|
return data
|
||||||
|
return {"ok": False, "msg": f"HTTP {r.status_code}", "orders": []}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "msg": str(e) or "拉取失败", "orders": [], "instance_id": inst.get("id")}
|
||||||
|
|
||||||
|
|
||||||
def pull_all_enabled() -> list[dict[str, Any]]:
|
def pull_all_enabled() -> list[dict[str, Any]]:
|
||||||
instances = list_instances(enabled_only=True)
|
instances = list_instances(enabled_only=True)
|
||||||
if not instances:
|
if not instances:
|
||||||
@@ -121,3 +207,24 @@ def pull_all_enabled() -> list[dict[str, Any]]:
|
|||||||
for inst in instances:
|
for inst in instances:
|
||||||
results.append(by_id.get(inst["id"]) or fetch_one_snapshot(inst))
|
results.append(by_id.get(inst["id"]) or fetch_one_snapshot(inst))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_cond_order_to_ids(instance_ids: list[int], payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
results = []
|
||||||
|
for iid in instance_ids:
|
||||||
|
inst = get_instance(int(iid))
|
||||||
|
if not inst:
|
||||||
|
results.append({"ok": False, "msg": "实例不存在", "instance_id": iid})
|
||||||
|
continue
|
||||||
|
if not inst.get("enabled"):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"msg": "实例未启用",
|
||||||
|
"instance_id": iid,
|
||||||
|
"instance_name": inst.get("name"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
results.append(post_cond_order(inst, payload))
|
||||||
|
return results
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
(function (global) {
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCandidates() {
|
||||||
|
const box = document.getElementById("cf-instances");
|
||||||
|
if (!box) return;
|
||||||
|
const r = await fetch("/api/cond_orders/candidates");
|
||||||
|
const d = await r.json();
|
||||||
|
const list = d.instances || [];
|
||||||
|
if (!list.length) {
|
||||||
|
box.innerHTML = '<span class="muted">暂无启用实例</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = list
|
||||||
|
.map(function (it) {
|
||||||
|
const disabled = !it.selectable;
|
||||||
|
const tip = disabled
|
||||||
|
? it.has_position
|
||||||
|
? "已有持仓"
|
||||||
|
: it.cond_order_locked
|
||||||
|
? "条件单占用"
|
||||||
|
: it.online
|
||||||
|
? "不可选"
|
||||||
|
: "离线"
|
||||||
|
: "可选";
|
||||||
|
return (
|
||||||
|
'<label class="check" title="' +
|
||||||
|
escapeHtml(tip) +
|
||||||
|
'">' +
|
||||||
|
'<input type="checkbox" name="cf-inst" value="' +
|
||||||
|
encodeURIComponent(String(it.id)) +
|
||||||
|
'"' +
|
||||||
|
(disabled ? " disabled" : "") +
|
||||||
|
"> " +
|
||||||
|
escapeHtml(it.name || "") +
|
||||||
|
' <span class="muted">(' +
|
||||||
|
escapeHtml(tip) +
|
||||||
|
")</span></label>"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<span class="muted">暂无条件单</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML =
|
||||||
|
'<div class="overview-table-wrap"><table class="overview-table"><thead><tr>' +
|
||||||
|
"<th>客户端</th><th>状态</th><th>标的</th><th>触发</th><th>偏离</th><th>到期h</th><th>出场</th><th>说明</th><th>操作</th>" +
|
||||||
|
"</tr></thead><tbody>" +
|
||||||
|
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 +=
|
||||||
|
'<button type="button" class="btn btn-sm btn-danger" data-cancel="' +
|
||||||
|
encodeURIComponent(o.hub_order_id || "") +
|
||||||
|
'" data-iid="' +
|
||||||
|
encodeURIComponent(String(o.instance_id || "")) +
|
||||||
|
'">取消</button>';
|
||||||
|
}
|
||||||
|
if (canExit) {
|
||||||
|
actions +=
|
||||||
|
'<button type="button" class="btn btn-sm" data-exit="' +
|
||||||
|
encodeURIComponent(o.hub_order_id || "") +
|
||||||
|
'" data-iid="' +
|
||||||
|
encodeURIComponent(String(o.instance_id || "")) +
|
||||||
|
'" data-target="' +
|
||||||
|
encodeURIComponent(o.target_index != null ? String(o.target_index) : "") +
|
||||||
|
'" data-pe="' +
|
||||||
|
(o.profit_exit_enabled ? "1" : "0") +
|
||||||
|
'" data-mult="' +
|
||||||
|
encodeURIComponent(String(o.profit_exit_mult || "1")) +
|
||||||
|
'">改出场</button>';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
"<tr>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml(o.instance_name || "") +
|
||||||
|
"</td>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml(statusLabel(o)) +
|
||||||
|
"</td>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) +
|
||||||
|
"</td>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml(side + " " + (o.trigger_px != null ? o.trigger_px : "")) +
|
||||||
|
"</td>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml(o.deviation_pct != null ? o.deviation_pct + "%" : "—") +
|
||||||
|
"</td>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml(o.min_expiry_hours != null ? String(o.min_expiry_hours) : "—") +
|
||||||
|
"</td>" +
|
||||||
|
"<td>" +
|
||||||
|
escapeHtml(exit || "—") +
|
||||||
|
"</td>" +
|
||||||
|
"<td class=\"muted\">" +
|
||||||
|
escapeHtml(o.message || "") +
|
||||||
|
"</td>" +
|
||||||
|
'<td><div class="overview-actions">' +
|
||||||
|
actions +
|
||||||
|
"</div></td>" +
|
||||||
|
"</tr>"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("") +
|
||||||
|
"</tbody></table></div>";
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
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);
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
global.HubCondOrders = { boot: boot, refresh: refresh };
|
||||||
|
})(window);
|
||||||
@@ -202,6 +202,18 @@ button, .btn {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.inst-pick {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 16px;
|
||||||
|
}
|
||||||
|
.inst-pick .check { margin: 0; }
|
||||||
|
.btn-sm {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.btn[disabled] {
|
.btn[disabled] {
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">总览</a>
|
<a href="/" class="{% if request.path == '/' %}active{% endif %}">总览</a>
|
||||||
|
<a href="/cond_orders" class="{% if request.path.startswith('/cond_orders') %}active{% endif %}">条件单</a>
|
||||||
<a href="/instances" class="{% if request.path.startswith('/instances') %}active{% endif %}">实例</a>
|
<a href="/instances" class="{% if request.path.startswith('/instances') %}active{% endif %}">实例</a>
|
||||||
<a href="/desk" class="{% if request.path.startswith('/desk') %}active{% endif %}">工作台</a>
|
<a href="/desk" class="{% if request.path.startswith('/desk') %}active{% endif %}">工作台</a>
|
||||||
<a href="/settings" class="{% if request.path.startswith('/settings') %}active{% endif %}">系统设置</a>
|
<a href="/settings" class="{% if request.path.startswith('/settings') %}active{% endif %}">系统设置</a>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}条件单 · 期权中控{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<section class="page-head">
|
||||||
|
<h1>条件开仓单</h1>
|
||||||
|
<button type="button" class="btn" id="btn-cond-refresh">刷新</button>
|
||||||
|
</section>
|
||||||
|
<p class="muted">中控只下发指令;行情监控与买币/开仓/平仓/卖币均在客户端本地执行。有仓或已占用的机器不可选。</p>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2 style="margin:0 0 10px;font-size:1rem">新建条件单</h2>
|
||||||
|
<div class="form-grid" id="cond-form">
|
||||||
|
<label>标的
|
||||||
|
<select id="cf-underlying">
|
||||||
|
<option value="ETH">ETH</option>
|
||||||
|
<option value="BTC">BTC</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>方向
|
||||||
|
<select id="cf-opt-type">
|
||||||
|
<option value="P">看跌 Put</option>
|
||||||
|
<option value="C">看涨 Call</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>触发
|
||||||
|
<select id="cf-trigger-side">
|
||||||
|
<option value="gte">指数 ≥ 触发价(上穿)</option>
|
||||||
|
<option value="lte">指数 ≤ 触发价(下穿)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>触发价
|
||||||
|
<input id="cf-trigger-px" type="number" step="any" placeholder="如 3000" required>
|
||||||
|
</label>
|
||||||
|
<label>偏离 %
|
||||||
|
<input id="cf-deviation" type="number" step="0.1" value="1" placeholder="1" required>
|
||||||
|
</label>
|
||||||
|
<label>最短到期(小时)
|
||||||
|
<input id="cf-expiry-h" type="number" step="1" value="24" placeholder="24" required>
|
||||||
|
</label>
|
||||||
|
<label>出场目标指数(可选)
|
||||||
|
<input id="cf-target" type="number" step="any" placeholder="如 2800">
|
||||||
|
</label>
|
||||||
|
<label class="check"><input id="cf-pe" type="checkbox" checked> 翻倍出场</label>
|
||||||
|
<label>翻倍倍数
|
||||||
|
<input id="cf-pe-mult" type="number" step="0.1" value="1" placeholder="1">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:12px">
|
||||||
|
<div class="muted" style="margin-bottom:6px">选择客户端</div>
|
||||||
|
<div id="cf-instances" class="inst-pick"></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:12px">
|
||||||
|
<button type="button" class="btn" id="btn-cond-create">下发条件单</button>
|
||||||
|
<span class="muted" id="cf-status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2 style="margin:0 0 10px;font-size:1rem">条件单列表</h2>
|
||||||
|
<div id="cond-list" class="muted">加载中…</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='cond_orders.js') }}"></script>
|
||||||
|
<script>HubCondOrders.boot();</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user