diff --git a/manual_trading_hub/README.md b/manual_trading_hub/README.md index 2192799..97ca041 100644 --- a/manual_trading_hub/README.md +++ b/manual_trading_hub/README.md @@ -1,8 +1,8 @@ # 手工交易多账户中控(manual_trading_hub) -> **完整操作说明见 [使用说明.md](./使用说明.md)**(监控区 / 下单区 / 系统设置、鉴权、四所能力与故障排查)。 +> **完整操作说明见 [使用说明.md](./使用说明.md)**(监控区 / 系统设置、鉴权、四所能力与故障排查)。 -本目录提供多账户 **监控 + 下单转发 + 紧急全平**。各 `crypto_monitor_*` 仅需注册 `hub_bridge`(见使用说明);策略与复盘仍在各实例网页。 +本目录提供多账户 **监控 + 紧急全平**。人工下单、关键位、趋势回调请在各 `crypto_monitor_*` 实例网页操作;策略与复盘仍在各实例。 --- diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 8d1fa45..3a0b9d8 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -1,6 +1,6 @@ """ -多账户交易中控:监控区 / 下单区 / 系统设置。 -转发至各 crypto_monitor_* 的 /api/hub/* 与子代理 /status。 +多账户交易中控:监控区 / 系统设置。 +聚合各实例监控数据与子代理 /status;下单请在各实例网页操作。 """ from __future__ import annotations @@ -9,7 +9,7 @@ import os from pathlib import Path import httpx -from fastapi import Body, FastAPI, HTTPException, Query, Request +from fastapi import Body, FastAPI, HTTPException, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field @@ -114,12 +114,18 @@ def root_redirect(): @app.get("/monitor") -@app.get("/trade") @app.get("/settings") def shell_pages(): return _shell_page() +@app.get("/trade") +def trade_removed_redirect(): + from fastapi.responses import RedirectResponse + + return RedirectResponse("/monitor", status_code=302) + + @app.get("/api/settings") def api_get_settings(): return load_settings() @@ -142,7 +148,7 @@ def api_settings_meta(): return { "env_disabled_ids": sorted(env_force_disabled_ids()), "hub_bridge_token_set": bool(HUB_BRIDGE_TOKEN), - "capability_options": ["order", "key", "trend"], + "capability_options": ["key", "trend"], "public_origin": f"{po[0]}://{po[1]}" if po else None, "public_origin_hint": ( "未设置 HUB_PUBLIC_ORIGIN 时,复盘链接若为 127.0.0.1,仅服务器本机浏览器可打开" @@ -182,25 +188,6 @@ async def _fetch_agent_status(client: httpx.AsyncClient, ex: dict) -> dict: } -def _exchange_brief(ex: dict | None) -> dict | None: - if not ex: - return None - return { - "id": ex.get("id"), - "key": ex.get("key"), - "name": ex.get("name"), - } - - -def _form_plain_dict(form) -> dict[str, str]: - out: dict[str, str] = {} - for k, v in form.multi_items() if hasattr(form, "multi_items") else form.items(): - if hasattr(v, "read"): - continue - out[str(k)] = "" if v is None else str(v) - return out - - def _parse_http_json_body(r: httpx.Response) -> dict: text = (r.text or "").strip() if not text: @@ -336,90 +323,6 @@ async def api_close_all(body: CloseAllBody | None = Body(default=None)): return {"results": list(results)} -@app.get("/api/trade/meta/{exchange_id}") -async def api_trade_meta(exchange_id: str): - ex = _find_exchange(exchange_id) - if not ex or not ex.get("enabled"): - raise HTTPException(status_code=404, detail="账户未启用") - async with httpx.AsyncClient() as client: - meta = await _fetch_flask_json(client, ex, "/api/hub/meta") - return {"exchange": ex, "meta": meta} - - -@app.post("/api/trade/order/{exchange_id}") -async def api_trade_order(exchange_id: str, request: Request): - ex = _find_exchange(exchange_id) - if not ex or not ex.get("enabled"): - raise HTTPException(status_code=404, detail="账户未启用") - try: - form = _form_plain_dict(await request.form()) - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, "/api/hub/add_order", "POST", form) - return {"exchange": _exchange_brief(ex), "result": result} - except HTTPException: - raise - except Exception as e: - return JSONResponse( - {"exchange": _exchange_brief(ex), "result": {"ok": False, "messages": [str(e)]}}, - status_code=200, - ) - - -@app.post("/api/trade/key/{exchange_id}") -async def api_trade_key(exchange_id: str, request: Request): - ex = _find_exchange(exchange_id) - if not ex or not ex.get("enabled"): - raise HTTPException(status_code=404, detail="账户未启用") - if "key" not in (ex.get("capabilities") or []): - raise HTTPException(status_code=400, detail="该账户不支持关键位") - try: - form = _form_plain_dict(await request.form()) - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, "/api/hub/add_key", "POST", form) - return {"exchange": _exchange_brief(ex), "result": result} - except HTTPException: - raise - except Exception as e: - return JSONResponse( - {"exchange": _exchange_brief(ex), "result": {"ok": False, "messages": [str(e)]}}, - status_code=200, - ) - - -@app.post("/api/trade/trend/preview/{exchange_id}") -async def api_trade_trend_preview(exchange_id: str, request: Request): - ex = _find_exchange(exchange_id) - if not ex or not ex.get("enabled"): - raise HTTPException(status_code=404, detail="账户未启用") - if "trend" not in (ex.get("capabilities") or []): - raise HTTPException(status_code=400, detail="该账户不支持趋势回调") - form = await request.form() - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, "/api/hub/trend/preview", "POST", dict(form)) - return {"exchange": ex, "result": result} - - -@app.post("/api/trade/trend/execute/{exchange_id}") -async def api_trade_trend_execute(exchange_id: str, request: Request): - ex = _find_exchange(exchange_id) - if not ex or not ex.get("enabled"): - raise HTTPException(status_code=404, detail="账户未启用") - form = await request.form() - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, "/api/hub/trend/execute", "POST", dict(form)) - return {"exchange": ex, "result": result} - - -@app.get("/api/trade/trend/preview/{exchange_id}/{preview_id}") -async def api_trade_trend_preview_get(exchange_id: str, preview_id: str): - ex = _find_exchange(exchange_id) - if not ex or not ex.get("enabled"): - raise HTTPException(status_code=404, detail="账户未启用") - async with httpx.AsyncClient() as client: - result = await _fetch_flask_json(client, ex, f"/api/hub/trend/preview/{preview_id}") - return {"exchange": ex, "result": result} - - def main(): import uvicorn diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 32c8ef5..a42d549 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -428,57 +428,7 @@ button:disabled { white-space: nowrap; } -/* —— 下单区 —— */ -.trade-bar { - display: flex; - flex-wrap: wrap; - gap: 12px; - align-items: center; - margin-bottom: 16px; - padding: 14px 16px; - background: var(--panel); - border: 1px solid var(--border); - border-radius: var(--radius); -} - -.trade-bar label { - font-size: 12px; - color: var(--muted); - margin-right: 6px; -} - -.trade-bar select { - min-width: 220px; -} - -.tabs { - display: flex; - gap: 4px; - margin-bottom: 16px; - padding: 4px; - background: var(--bg-elevated); - border-radius: var(--radius); - border: 1px solid var(--border-soft); - width: fit-content; - max-width: 100%; - flex-wrap: wrap; -} - -.tabs button { - border: none; - background: transparent; - padding: 8px 16px; - border-radius: 7px; -} - -.tabs button.active { - background: var(--panel); - color: var(--accent); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - border: 1px solid var(--border); -} - -.trade-meta { +.settings-meta-line { font-size: 12px; color: var(--muted); padding: 10px 14px; @@ -489,21 +439,6 @@ button:disabled { line-height: 1.55; } -.form-panel.hidden { - display: none; -} - -.form-panel .card-head strong { - font-size: 14px; -} - -.form-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 12px; - align-items: end; -} - .field { display: flex; flex-direction: column; @@ -522,7 +457,6 @@ button:disabled { .field input, .field select, -.trade-bar select, .form-row input, .form-row select { background: var(--bg); diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index ea90543..f4ec628 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -1,8 +1,6 @@ (function () { const toast = document.getElementById("toast"); let settingsCache = null; - let tradeMeta = {}; - let trendPreviewId = null; let monitorTimer = null; function showToast(msg, isErr) { @@ -34,7 +32,6 @@ function currentPage() { const p = window.location.pathname.replace(/\/$/, "") || "/monitor"; - if (p.includes("trade")) return "trade"; if (p.includes("settings")) return "settings"; return "monitor"; } @@ -49,7 +46,6 @@ }); if (page === "monitor") startMonitorPoll(); else stopMonitorPoll(); - if (page === "trade") initTradePage(); if (page === "settings") loadSettingsUI(); } @@ -171,13 +167,18 @@ const review = row.review_url ? `复盘` : ""; + const flaskOpen = row.flask_url_browser || row.flask_url; + const openFlask = flaskOpen + ? `实例` + : ""; return `
${esc(row.name)}
-
${esc(row.flask_url_browser || row.flask_url || "")}
+
${esc(flaskOpen || "")}
+ ${openFlask} ${review}
@@ -215,164 +216,6 @@ } } - function initTradePage() { - loadSettings().then(() => { - const sel = document.getElementById("trade-account"); - const prev = sel.value; - sel.innerHTML = enabledAccounts() - .map( - (x) => - `` - ) - .join(""); - if (prev) sel.value = prev; - syncTradeTabs(); - loadTradeMeta(); - }); - } - - function accountCaps() { - const id = document.getElementById("trade-account").value; - const ex = (settingsCache?.exchanges || []).find((x) => String(x.id) === String(id)); - return ex?.capabilities || []; - } - - function syncTradeTabs() { - const caps = accountCaps(); - document.querySelectorAll(".tabs button").forEach((btn) => { - const tab = btn.dataset.tab; - let ok = false; - if (tab === "order") ok = caps.includes("order"); - if (tab === "key") ok = caps.includes("key"); - if (tab === "trend") ok = caps.includes("trend"); - btn.disabled = !ok; - btn.style.opacity = ok ? "1" : "0.4"; - }); - let active = document.querySelector(".tabs button.active"); - if (active && active.disabled) { - const first = [...document.querySelectorAll(".tabs button")].find((b) => !b.disabled); - if (first) switchTradeTab(first.dataset.tab); - } - ["order", "key", "trend"].forEach((t) => { - document.getElementById("panel-" + t).classList.toggle( - "hidden", - !document.querySelector(`.tabs button[data-tab="${t}"]`).classList.contains("active") - ); - }); - } - - function switchTradeTab(tab) { - document.querySelectorAll(".tabs button").forEach((b) => { - b.classList.toggle("active", b.dataset.tab === tab); - }); - ["order", "key", "trend"].forEach((t) => { - document.getElementById("panel-" + t).classList.toggle("hidden", t !== tab); - }); - trendPreviewId = null; - document.getElementById("trend-preview-box").style.display = "none"; - } - - async function loadTradeMeta() { - const id = document.getElementById("trade-account").value; - if (!id) return; - try { - const r = await fetch("/api/trade/meta/" + encodeURIComponent(id)); - const data = await r.json(); - tradeMeta = data.meta?.meta || data.meta || {}; - const el = document.getElementById("trade-meta"); - let txt = ""; - if (tradeMeta.key_gate_rule_text) txt = tradeMeta.key_gate_rule_text; - else if (tradeMeta.trend_pullback_preview_ttl) { - txt = `预览 ${tradeMeta.trend_pullback_preview_ttl}s · 补仓 ${tradeMeta.trend_pullback_dca_legs} 档 · 余额偏差 ≤${tradeMeta.trend_preview_max_drift_pct}%`; - } - el.textContent = txt; - el.style.display = txt ? "block" : "none"; - } catch (e) { - const el = document.getElementById("trade-meta"); - el.textContent = ""; - el.style.display = "none"; - } - } - - async function parseJsonResponse(r) { - const text = await r.text(); - if (!text) return {}; - try { - return JSON.parse(text); - } catch (e) { - const snippet = text.slice(0, 200); - throw new Error( - `HTTP ${r.status} 响应不是 JSON:${snippet}${text.length > 200 ? "…" : ""}` - ); - } - } - - async function submitForm(path, formEl) { - const id = document.getElementById("trade-account").value; - const fd = new FormData(formEl); - try { - const r = await fetch(path + encodeURIComponent(id), { method: "POST", body: fd }); - const j = await parseJsonResponse(r); - const res = j.result || {}; - const msgs = - (res.messages || []).join("\n") || - res.error || - res.msg || - res.text || - JSON.stringify(res, null, 2); - const failed = res.ok === false || r.status >= 400 || !!res.error || !!res.text; - showToast(msgs || (failed ? "操作失败" : "已提交"), failed); - if (res.ok && res.preview) { - showTrendPreview(res); - } - loadTradeMeta(); - } catch (e) { - showToast(String(e), true); - } - } - - function showTrendPreview(res) { - trendPreviewId = res.preview_id; - const p = res.preview || {}; - const box = document.getElementById("trend-preview-box"); - const levels = (p.grid_levels || []) - .map((r) => `${r.i}${r.price}${r.contracts}`) - .join(""); - box.innerHTML = ` -
预览 #${esc(p.id || trendPreviewId)} · ${p.expires_in_sec ?? "?"}s
-
${esc(p.symbol)} ${esc(p.direction)} · ${p.leverage}x · 快照 ${fmt(p.snapshot_available_usdt, 2)} U
- ${levels}
#补仓价张数
-
- -
`; - box.style.display = "block"; - document.getElementById("btn-trend-exec").onclick = executeTrend; - } - - async function executeTrend() { - if (!trendPreviewId) { - showToast("请先生成预览", true); - return; - } - if (!confirm("确认按预览参数实盘下单?")) return; - const id = document.getElementById("trade-account").value; - const fd = new FormData(); - fd.set("preview_id", trendPreviewId); - try { - const r = await fetch("/api/trade/trend/execute/" + encodeURIComponent(id), { - method: "POST", - body: fd, - }); - const j = await r.json(); - const res = j.result || {}; - showToast((res.messages || []).join("\n") || JSON.stringify(res), !res.ok); - document.getElementById("trend-preview-box").style.display = "none"; - trendPreviewId = null; - } catch (e) { - showToast(String(e), true); - } - } - async function loadSettingsMetaLine() { try { const r = await fetch("/api/settings/meta"); @@ -425,9 +268,8 @@
- - - + +
@@ -442,7 +284,6 @@ version: 1, exchanges: rows.map((card) => { const caps = []; - if (card.querySelector(".cap-order").checked) caps.push("order"); if (card.querySelector(".cap-key").checked) caps.push("key"); if (card.querySelector(".cap-trend").checked) caps.push("trend"); const id = card.querySelector(".ex-id").value.trim(); @@ -482,44 +323,6 @@ document.getElementById("btn-monitor-refresh").onclick = loadMonitorBoard; document.getElementById("auto-monitor").onchange = startMonitorPoll; document.getElementById("btn-close-all").onclick = closeAll; - document.getElementById("trade-account").onchange = () => { - syncTradeTabs(); - loadTradeMeta(); - }; - document.querySelectorAll(".tabs button").forEach((btn) => { - btn.onclick = () => { - if (!btn.disabled) switchTradeTab(btn.dataset.tab); - }; - }); - document.getElementById("form-order").onsubmit = (e) => { - e.preventDefault(); - submitForm("/api/trade/order/", e.target); - }; - document.getElementById("form-key").onsubmit = (e) => { - e.preventDefault(); - submitForm("/api/trade/key/", e.target); - }; - document.getElementById("form-trend").onsubmit = (e) => { - e.preventDefault(); - submitForm("/api/trade/trend/preview/", e.target); - }; - document.getElementById("order-sltp-mode").onchange = function () { - const pct = this.value === "pct"; - const slField = document.getElementById("order-sl").closest(".field"); - const tpField = document.getElementById("order-tp").closest(".field"); - if (slField) slField.style.display = pct ? "none" : ""; - if (tpField) tpField.style.display = pct ? "none" : ""; - document.getElementById("wrap-sl-pct").style.display = pct ? "" : "none"; - document.getElementById("wrap-tp-pct").style.display = pct ? "" : "none"; - }; - document.getElementById("key-sl-tp-mode").onchange = function () { - const manual = this.value === "trend_manual"; - document.getElementById("wrap-key-manual-tp").style.display = manual ? "" : "none"; - }; - document.getElementById("trend-direction").onchange = function () { - const lbl = document.getElementById("trend-add-label"); - if (lbl) lbl.textContent = this.value === "short" ? "补仓下沿价" : "补仓上沿价"; - }; document.getElementById("btn-settings-save").onclick = saveSettings; document.getElementById("btn-settings-reload").onclick = loadSettingsUI; document.getElementById("btn-settings-add").onclick = () => { @@ -533,7 +336,7 @@ agent_url: "http://127.0.0.1:15200", review_url: "", enabled: false, - capabilities: ["order"], + capabilities: ["key"], }); settingsCache = data; loadSettingsUI(); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index bb76456..d1e625a 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -12,7 +12,6 @@
复盘系统中控
@@ -25,6 +24,7 @@ 说明:数据来源与复盘链接
持仓与余额来自子代理;关键位、机器人单、趋势计划来自各实例 Flask(须 PM2 运行 crypto_*)。
+ 人工下单、添加关键位、趋势回调请在各实例网页操作;中控仅监控与紧急全平。
「交易复盘」在新标签打开该实例 /records。其它电脑访问中控时,请在 hub 的 .env 设置 HUB_PUBLIC_ORIGIN=http://服务器内网IP
@@ -41,185 +41,6 @@
- -