删除中控下单区
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
# 手工交易多账户中控(manual_trading_hub)
|
# 手工交易多账户中控(manual_trading_hub)
|
||||||
|
|
||||||
> **完整操作说明见 [使用说明.md](./使用说明.md)**(监控区 / 下单区 / 系统设置、鉴权、四所能力与故障排查)。
|
> **完整操作说明见 [使用说明.md](./使用说明.md)**(监控区 / 系统设置、鉴权、四所能力与故障排查)。
|
||||||
|
|
||||||
本目录提供多账户 **监控 + 下单转发 + 紧急全平**。各 `crypto_monitor_*` 仅需注册 `hub_bridge`(见使用说明);策略与复盘仍在各实例网页。
|
本目录提供多账户 **监控 + 紧急全平**。人工下单、关键位、趋势回调请在各 `crypto_monitor_*` 实例网页操作;策略与复盘仍在各实例。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+11
-108
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
多账户交易中控:监控区 / 下单区 / 系统设置。
|
多账户交易中控:监控区 / 系统设置。
|
||||||
转发至各 crypto_monitor_* 的 /api/hub/* 与子代理 /status。
|
聚合各实例监控数据与子代理 /status;下单请在各实例网页操作。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
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.responses import FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -114,12 +114,18 @@ def root_redirect():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/monitor")
|
@app.get("/monitor")
|
||||||
@app.get("/trade")
|
|
||||||
@app.get("/settings")
|
@app.get("/settings")
|
||||||
def shell_pages():
|
def shell_pages():
|
||||||
return _shell_page()
|
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")
|
@app.get("/api/settings")
|
||||||
def api_get_settings():
|
def api_get_settings():
|
||||||
return load_settings()
|
return load_settings()
|
||||||
@@ -142,7 +148,7 @@ def api_settings_meta():
|
|||||||
return {
|
return {
|
||||||
"env_disabled_ids": sorted(env_force_disabled_ids()),
|
"env_disabled_ids": sorted(env_force_disabled_ids()),
|
||||||
"hub_bridge_token_set": bool(HUB_BRIDGE_TOKEN),
|
"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": f"{po[0]}://{po[1]}" if po else None,
|
||||||
"public_origin_hint": (
|
"public_origin_hint": (
|
||||||
"未设置 HUB_PUBLIC_ORIGIN 时,复盘链接若为 127.0.0.1,仅服务器本机浏览器可打开"
|
"未设置 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:
|
def _parse_http_json_body(r: httpx.Response) -> dict:
|
||||||
text = (r.text or "").strip()
|
text = (r.text or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
@@ -336,90 +323,6 @@ async def api_close_all(body: CloseAllBody | None = Body(default=None)):
|
|||||||
return {"results": list(results)}
|
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():
|
def main():
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
@@ -428,57 +428,7 @@ button:disabled {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 下单区 —— */
|
.settings-meta-line {
|
||||||
.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 {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
@@ -489,21 +439,6 @@ button:disabled {
|
|||||||
line-height: 1.55;
|
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 {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -522,7 +457,6 @@ button:disabled {
|
|||||||
|
|
||||||
.field input,
|
.field input,
|
||||||
.field select,
|
.field select,
|
||||||
.trade-bar select,
|
|
||||||
.form-row input,
|
.form-row input,
|
||||||
.form-row select {
|
.form-row select {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const toast = document.getElementById("toast");
|
const toast = document.getElementById("toast");
|
||||||
let settingsCache = null;
|
let settingsCache = null;
|
||||||
let tradeMeta = {};
|
|
||||||
let trendPreviewId = null;
|
|
||||||
let monitorTimer = null;
|
let monitorTimer = null;
|
||||||
|
|
||||||
function showToast(msg, isErr) {
|
function showToast(msg, isErr) {
|
||||||
@@ -34,7 +32,6 @@
|
|||||||
|
|
||||||
function currentPage() {
|
function currentPage() {
|
||||||
const p = window.location.pathname.replace(/\/$/, "") || "/monitor";
|
const p = window.location.pathname.replace(/\/$/, "") || "/monitor";
|
||||||
if (p.includes("trade")) return "trade";
|
|
||||||
if (p.includes("settings")) return "settings";
|
if (p.includes("settings")) return "settings";
|
||||||
return "monitor";
|
return "monitor";
|
||||||
}
|
}
|
||||||
@@ -49,7 +46,6 @@
|
|||||||
});
|
});
|
||||||
if (page === "monitor") startMonitorPoll();
|
if (page === "monitor") startMonitorPoll();
|
||||||
else stopMonitorPoll();
|
else stopMonitorPoll();
|
||||||
if (page === "trade") initTradePage();
|
|
||||||
if (page === "settings") loadSettingsUI();
|
if (page === "settings") loadSettingsUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,13 +167,18 @@
|
|||||||
const review = row.review_url
|
const review = row.review_url
|
||||||
? `<a class="btn-link" href="${esc(row.review_url)}" target="_blank" rel="noopener">复盘</a>`
|
? `<a class="btn-link" href="${esc(row.review_url)}" target="_blank" rel="noopener">复盘</a>`
|
||||||
: "";
|
: "";
|
||||||
|
const flaskOpen = row.flask_url_browser || row.flask_url;
|
||||||
|
const openFlask = flaskOpen
|
||||||
|
? `<a class="btn-link" href="${esc(flaskOpen)}" target="_blank" rel="noopener">实例</a>`
|
||||||
|
: "";
|
||||||
return `<div class="card">
|
return `<div class="card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<div>
|
<div>
|
||||||
<div class="card-title">${esc(row.name)}</div>
|
<div class="card-title">${esc(row.name)}</div>
|
||||||
<div class="card-sub">${esc(row.flask_url_browser || row.flask_url || "")}</div>
|
<div class="card-sub">${esc(flaskOpen || "")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
${openFlask}
|
||||||
${review}
|
${review}
|
||||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,164 +216,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTradePage() {
|
|
||||||
loadSettings().then(() => {
|
|
||||||
const sel = document.getElementById("trade-account");
|
|
||||||
const prev = sel.value;
|
|
||||||
sel.innerHTML = enabledAccounts()
|
|
||||||
.map(
|
|
||||||
(x) =>
|
|
||||||
`<option value="${esc(x.id)}">${esc(x.name)}</option>`
|
|
||||||
)
|
|
||||||
.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) => `<tr><td>${r.i}</td><td>${r.price}</td><td>${r.contracts}</td></tr>`)
|
|
||||||
.join("");
|
|
||||||
box.innerHTML = `
|
|
||||||
<div class="section-title">预览 #${esc(p.id || trendPreviewId)} · ${p.expires_in_sec ?? "?"}s</div>
|
|
||||||
<div class="list-line">${esc(p.symbol)} ${esc(p.direction)} · ${p.leverage}x · 快照 ${fmt(p.snapshot_available_usdt, 2)} U</div>
|
|
||||||
<table class="data-table"><thead><tr><th>#</th><th>补仓价</th><th>张数</th></tr></thead><tbody>${levels}</tbody></table>
|
|
||||||
<div class="form-row" style="margin-top:8px">
|
|
||||||
<button type="button" id="btn-trend-exec">确认执行(实盘)</button>
|
|
||||||
</div>`;
|
|
||||||
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() {
|
async function loadSettingsMetaLine() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch("/api/settings/meta");
|
const r = await fetch("/api/settings/meta");
|
||||||
@@ -425,9 +268,8 @@
|
|||||||
<div class="field field-wide"><label>复盘链接(可空)</label><input class="ex-review" value="${esc(ex.review_url || "")}" placeholder="留空则自动生成 /records" /></div>
|
<div class="field field-wide"><label>复盘链接(可空)</label><input class="ex-review" value="${esc(ex.review_url || "")}" placeholder="留空则自动生成 /records" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cap-chips">
|
<div class="cap-chips">
|
||||||
<label><input type="checkbox" class="cap-order" ${caps.includes("order") ? "checked" : ""}/> 下单</label>
|
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/> 监控关键位</label>
|
||||||
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/> 关键位</label>
|
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/> 监控趋势计划</label>
|
||||||
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/> 趋势</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-card-foot">
|
<div class="settings-card-foot">
|
||||||
<div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
|
<div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
|
||||||
@@ -442,7 +284,6 @@
|
|||||||
version: 1,
|
version: 1,
|
||||||
exchanges: rows.map((card) => {
|
exchanges: rows.map((card) => {
|
||||||
const caps = [];
|
const caps = [];
|
||||||
if (card.querySelector(".cap-order").checked) caps.push("order");
|
|
||||||
if (card.querySelector(".cap-key").checked) caps.push("key");
|
if (card.querySelector(".cap-key").checked) caps.push("key");
|
||||||
if (card.querySelector(".cap-trend").checked) caps.push("trend");
|
if (card.querySelector(".cap-trend").checked) caps.push("trend");
|
||||||
const id = card.querySelector(".ex-id").value.trim();
|
const id = card.querySelector(".ex-id").value.trim();
|
||||||
@@ -482,44 +323,6 @@
|
|||||||
document.getElementById("btn-monitor-refresh").onclick = loadMonitorBoard;
|
document.getElementById("btn-monitor-refresh").onclick = loadMonitorBoard;
|
||||||
document.getElementById("auto-monitor").onchange = startMonitorPoll;
|
document.getElementById("auto-monitor").onchange = startMonitorPoll;
|
||||||
document.getElementById("btn-close-all").onclick = closeAll;
|
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-save").onclick = saveSettings;
|
||||||
document.getElementById("btn-settings-reload").onclick = loadSettingsUI;
|
document.getElementById("btn-settings-reload").onclick = loadSettingsUI;
|
||||||
document.getElementById("btn-settings-add").onclick = () => {
|
document.getElementById("btn-settings-add").onclick = () => {
|
||||||
@@ -533,7 +336,7 @@
|
|||||||
agent_url: "http://127.0.0.1:15200",
|
agent_url: "http://127.0.0.1:15200",
|
||||||
review_url: "",
|
review_url: "",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
capabilities: ["order"],
|
capabilities: ["key"],
|
||||||
});
|
});
|
||||||
settingsCache = data;
|
settingsCache = data;
|
||||||
loadSettingsUI();
|
loadSettingsUI();
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<div class="brand">复盘系统中控</div>
|
<div class="brand">复盘系统中控</div>
|
||||||
<nav class="top-nav">
|
<nav class="top-nav">
|
||||||
<a href="/monitor" id="nav-monitor">监控区</a>
|
<a href="/monitor" id="nav-monitor">监控区</a>
|
||||||
<a href="/trade" id="nav-trade">下单区</a>
|
|
||||||
<a href="/settings" id="nav-settings">系统设置</a>
|
<a href="/settings" id="nav-settings">系统设置</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
<summary>说明:数据来源与复盘链接</summary>
|
<summary>说明:数据来源与复盘链接</summary>
|
||||||
<div class="hint-body">
|
<div class="hint-body">
|
||||||
持仓与余额来自子代理;关键位、机器人单、趋势计划来自各实例 Flask(须 PM2 运行 crypto_*)。<br />
|
持仓与余额来自子代理;关键位、机器人单、趋势计划来自各实例 Flask(须 PM2 运行 crypto_*)。<br />
|
||||||
|
人工下单、添加关键位、趋势回调请在各实例网页操作;中控仅监控与紧急全平。<br />
|
||||||
「交易复盘」在新标签打开该实例 /records。其它电脑访问中控时,请在 hub 的 <code>.env</code> 设置
|
「交易复盘」在新标签打开该实例 /records。其它电脑访问中控时,请在 hub 的 <code>.env</code> 设置
|
||||||
<code>HUB_PUBLIC_ORIGIN=http://服务器内网IP</code>。
|
<code>HUB_PUBLIC_ORIGIN=http://服务器内网IP</code>。
|
||||||
</div>
|
</div>
|
||||||
@@ -41,185 +41,6 @@
|
|||||||
<div id="monitor-grid" class="grid-monitor"></div>
|
<div id="monitor-grid" class="grid-monitor"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-trade" class="page hidden">
|
|
||||||
<div class="page-head">
|
|
||||||
<h1>下单区</h1>
|
|
||||||
</div>
|
|
||||||
<div class="trade-bar">
|
|
||||||
<label for="trade-account">交易账户</label>
|
|
||||||
<select id="trade-account"></select>
|
|
||||||
</div>
|
|
||||||
<div class="tabs">
|
|
||||||
<button type="button" data-tab="order" class="active">人工下单</button>
|
|
||||||
<button type="button" data-tab="key">关键位</button>
|
|
||||||
<button type="button" data-tab="trend">趋势回调</button>
|
|
||||||
</div>
|
|
||||||
<div id="trade-meta" class="trade-meta" style="display:none"></div>
|
|
||||||
|
|
||||||
<div id="panel-order" class="form-panel card">
|
|
||||||
<div class="card-head"><strong>人工下单</strong></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form id="form-order" class="form-grid">
|
|
||||||
<div class="field">
|
|
||||||
<label>合约</label>
|
|
||||||
<input name="symbol" placeholder="BTC / BTCUSDT" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>方向</label>
|
|
||||||
<select name="direction" required>
|
|
||||||
<option value="">请选择</option>
|
|
||||||
<option value="long">做多</option>
|
|
||||||
<option value="short">做空</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>止盈止损</label>
|
|
||||||
<select name="sltp_mode" id="order-sltp-mode">
|
|
||||||
<option value="price">价格</option>
|
|
||||||
<option value="pct">百分比</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>单型</label>
|
|
||||||
<select name="trade_style" required>
|
|
||||||
<option value="trend">趋势单</option>
|
|
||||||
<option value="swing">波段单</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>杠杆</label>
|
|
||||||
<input name="leverage" type="number" min="1" step="1" placeholder="可选" />
|
|
||||||
</div>
|
|
||||||
<div class="field field-check">
|
|
||||||
<input type="checkbox" name="breakeven_enabled" value="1" id="order-be" checked />
|
|
||||||
<label for="order-be">移动保本</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>止损</label>
|
|
||||||
<input name="sl" id="order-sl" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>止盈</label>
|
|
||||||
<input name="tgt" id="order-tp" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="field" id="wrap-sl-pct" style="display:none">
|
|
||||||
<label>止损 %</label>
|
|
||||||
<input name="sl_pct" id="order-sl-pct" type="number" step="0.01" />
|
|
||||||
</div>
|
|
||||||
<div class="field" id="wrap-tp-pct" style="display:none">
|
|
||||||
<label>止盈 %</label>
|
|
||||||
<input name="tp_pct" id="order-tp-pct" type="number" step="0.01" />
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="primary">开仓(以损定仓)</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="panel-key" class="form-panel card hidden">
|
|
||||||
<div class="card-head"><strong>添加关键位</strong></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form id="form-key" class="form-grid">
|
|
||||||
<div class="field">
|
|
||||||
<label>合约</label>
|
|
||||||
<input name="symbol" placeholder="BTC / BTCUSDT" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>类型</label>
|
|
||||||
<select name="type" required>
|
|
||||||
<option value="箱体突破">箱体突破</option>
|
|
||||||
<option value="收敛突破">收敛突破</option>
|
|
||||||
<option value="斐波回调0.618">斐波回调0.618</option>
|
|
||||||
<option value="斐波回调0.786">斐波回调0.786</option>
|
|
||||||
<option value="关键阻力位">关键阻力位</option>
|
|
||||||
<option value="关键支撑位">关键支撑位</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>方向</label>
|
|
||||||
<select name="direction" required>
|
|
||||||
<option value="">请选择</option>
|
|
||||||
<option value="long">做多</option>
|
|
||||||
<option value="short">做空</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>上沿 / 阻力</label>
|
|
||||||
<input name="upper" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>下沿 / 支撑</label>
|
|
||||||
<input name="lower" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>模式</label>
|
|
||||||
<select name="sl_tp_mode" id="key-sl-tp-mode">
|
|
||||||
<option value="standard">标准突破</option>
|
|
||||||
<option value="box_1p5">箱体1R·止盈1.5H</option>
|
|
||||||
<option value="trend_manual">趋势单·自填止盈</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field" id="wrap-key-manual-tp" style="display:none">
|
|
||||||
<label>趋势止盈价</label>
|
|
||||||
<input name="manual_take_profit" id="key-manual-tp" step="any" />
|
|
||||||
</div>
|
|
||||||
<div class="field field-check">
|
|
||||||
<input type="checkbox" name="breakeven_enabled" value="1" id="key-be-cb" />
|
|
||||||
<label for="key-be-cb">移动保本</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="primary">添加关键位</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="panel-trend" class="form-panel card hidden">
|
|
||||||
<div class="card-head"><strong>趋势回调</strong></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form id="form-trend" class="form-grid">
|
|
||||||
<div class="field">
|
|
||||||
<label>合约</label>
|
|
||||||
<input name="symbol" placeholder="BTC / ETHUSDT" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>方向</label>
|
|
||||||
<select name="direction" id="trend-direction" required>
|
|
||||||
<option value="">请选择</option>
|
|
||||||
<option value="long">做多</option>
|
|
||||||
<option value="short">做空</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>杠杆</label>
|
|
||||||
<input name="leverage" type="number" min="1" step="1" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>风险 %</label>
|
|
||||||
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>止损价</label>
|
|
||||||
<input name="sl" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label id="trend-add-label">补仓上沿价</label>
|
|
||||||
<input name="add_upper" id="trend-add-upper" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>止盈价</label>
|
|
||||||
<input name="take_profit" step="any" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="primary">生成预览</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="trend-preview-box" style="margin-top:16px;display:none"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="page-settings" class="page hidden">
|
<div id="page-settings" class="page hidden">
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1>系统设置</h1>
|
<h1>系统设置</h1>
|
||||||
@@ -231,7 +52,7 @@
|
|||||||
<code>HUB_DISABLED_IDS</code> 可强制关闭账户;<code>HUB_BRIDGE_TOKEN</code> 与实例一致,或实例 <code>APP_AUTH_DISABLED=true</code>。
|
<code>HUB_DISABLED_IDS</code> 可强制关闭账户;<code>HUB_BRIDGE_TOKEN</code> 与实例一致,或实例 <code>APP_AUTH_DISABLED=true</code>。
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<p id="settings-meta-line" class="trade-meta"></p>
|
<p id="settings-meta-line" class="settings-meta-line"></p>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button type="button" id="btn-settings-save" class="primary">保存设置</button>
|
<button type="button" id="btn-settings-save" class="primary">保存设置</button>
|
||||||
<button type="button" id="btn-settings-add">添加交易所</button>
|
<button type="button" id="btn-settings-add">添加交易所</button>
|
||||||
|
|||||||
+23
-61
@@ -1,6 +1,6 @@
|
|||||||
# 多账户交易中控 — 使用说明
|
# 多账户交易中控 — 使用说明
|
||||||
|
|
||||||
本文档说明 **manual_trading_hub**(方案 A)的架构、启动方式、三页界面操作与故障排查。中控聚合四所 **监控 + 下单 + 关键位 + 趋势回调(仅 Gate 趋势户)**;**交易复盘**仍进入各实例网页,中控只提供跳转链接。
|
本文档说明 **manual_trading_hub** 的架构、启动方式、界面操作与故障排查。中控聚合四所 **持仓/余额/关键位/趋势计划监控 + 紧急全平**;**人工下单、添加关键位、趋势回调、交易复盘** 均在各实例网页操作。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,17 +9,16 @@
|
|||||||
```
|
```
|
||||||
浏览器
|
浏览器
|
||||||
├─ /monitor 监控区(持仓、关键位、趋势计划、全平)
|
├─ /monitor 监控区(持仓、关键位、趋势计划、全平)
|
||||||
├─ /trade 下单区(人工下单 / 关键位 / 趋势回调)
|
|
||||||
└─ /settings 系统设置(hub_settings.json)
|
└─ /settings 系统设置(hub_settings.json)
|
||||||
|
|
||||||
中控 hub.py(默认 :5100)
|
中控 hub.py(默认 :5100)
|
||||||
├─ HTTP → 子代理 agent.py × N(/status、/emergency/close-all)
|
├─ HTTP → 子代理 agent.py × N(/status、/emergency/close-all)
|
||||||
└─ HTTP → 各实例 Flask app.py(/api/hub/*、/api/price_snapshot)
|
└─ HTTP → 各实例 Flask(/api/hub/monitor、/api/price_snapshot 等只读聚合)
|
||||||
```
|
```
|
||||||
|
|
||||||
| 组件 | 职责 | 默认端口(可在设置页改) |
|
| 组件 | 职责 | 默认端口(可在设置页改) |
|
||||||
|------|------|-------------------------|
|
|------|------|-------------------------|
|
||||||
| **hub.py** | 聚合 UI、转发下单/监控 API、全平 | `5100` |
|
| **hub.py** | 聚合 UI、监控 API、全平 | `5100` |
|
||||||
| **agent.py** | 交易所只读状态 + 紧急市价全平 | 币安 `15200`、OKX `15201`、Gate `15202`、Gate趋势 `15203` |
|
| **agent.py** | 交易所只读状态 + 紧急市价全平 | 币安 `15200`、OKX `15201`、Gate `15202`、Gate趋势 `15203` |
|
||||||
| **crypto_monitor_*.app** | 策略库、关键位、人工单、趋势预览/执行 | 币安 `5001`、Gate `5000`、Gate趋势 `5002`、OKX `5004` |
|
| **crypto_monitor_*.app** | 策略库、关键位、人工单、趋势预览/执行 | 币安 `5001`、Gate `5000`、Gate趋势 `5002`、OKX `5004` |
|
||||||
|
|
||||||
@@ -139,8 +138,8 @@ python hub.py
|
|||||||
浏览器打开:
|
浏览器打开:
|
||||||
|
|
||||||
- 监控区:http://127.0.0.1:5100/monitor
|
- 监控区:http://127.0.0.1:5100/monitor
|
||||||
- 下单区:http://127.0.0.1:5100/trade
|
|
||||||
- 系统设置:http://127.0.0.1:5100/settings
|
- 系统设置:http://127.0.0.1:5100/settings
|
||||||
|
- (旧链接 `/trade` 会自动跳转到监控区)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -154,7 +153,7 @@ python hub.py
|
|||||||
| **机器人持仓** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active) |
|
| **机器人持仓** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active) |
|
||||||
| **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) |
|
| **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) |
|
||||||
| **趋势计划** | 仅 Gate 趋势户;`trend_pullback_plans` active |
|
| **趋势计划** | 仅 Gate 趋势户;`trend_pullback_plans` active |
|
||||||
| **交易复盘** | 新标签打开该户 `/records`;**中控不做复盘**。链接默认由 `flask_url` 生成;若配置 **`HUB_PUBLIC_ORIGIN`**(如 `http://192.168.x.x`),会把 `127.0.0.1` 换成 Ubuntu 内网 IP,方便局域网其它设备打开 |
|
| **实例 / 复盘** | 「实例」打开该户 Flask 首页;「复盘」打开 `/records`。**中控不做下单与复盘编辑**。若配置 **`HUB_PUBLIC_ORIGIN`**,外链会把 `127.0.0.1` 换成内网 IP |
|
||||||
| **关键位** | 来自实例 `/api/hub/monitor` + `/api/price_snapshot`(须 Flask 已启动);无记录或 Flask 未连通时卡片会提示原因;**Gate 趋势户**无关键位 |
|
| **关键位** | 来自实例 `/api/hub/monitor` + `/api/price_snapshot`(须 Flask 已启动);无记录或 Flask 未连通时卡片会提示原因;**Gate 趋势户**无关键位 |
|
||||||
| **该户全平** | `POST` 子代理 `/emergency/close-all`,仅平该 API Key 仓位 |
|
| **该户全平** | `POST` 子代理 `/emergency/close-all`,仅平该 API Key 仓位 |
|
||||||
| **全局紧急全平** | 对所有已启用户依次全平(不含 `HUB_DISABLED_IDS` 强制关闭的 id) |
|
| **全局紧急全平** | 对所有已启用户依次全平(不含 `HUB_DISABLED_IDS` 强制关闭的 id) |
|
||||||
@@ -162,36 +161,11 @@ python hub.py
|
|||||||
|
|
||||||
持仓数据以 **子代理 ccxt** 为准;关键位/趋势/机器人单以 **Flask 数据库** 为准。若 Flask 未启动,卡片仍会显示 agent 持仓,但下方策略信息可能为空或报错。
|
持仓数据以 **子代理 ccxt** 为准;关键位/趋势/机器人单以 **Flask 数据库** 为准。若 Flask 未启动,卡片仍会显示 agent 持仓,但下方策略信息可能为空或报错。
|
||||||
|
|
||||||
### 4.2 下单区 `/trade`
|
### 4.2 系统设置 `/settings`
|
||||||
|
|
||||||
顶部 **账户下拉** 切换目标所;下方 Tab 根据该户 **能力** 自动启用/禁用:
|
**可用**:打开 http://127.0.0.1:5100/settings ,修改表格后点 **保存设置** 即写入 `hub_settings.json`;**重新加载** 从磁盘/默认再读(会重新套用 `HUB_DISABLED_IDS`)。保存后监控区立即使用新 URL/启用状态,**无需重启 hub**。
|
||||||
|
|
||||||
#### Tab:人工下单
|
**下单与关键位**:请在监控卡片点击「实例」,进入各 `crypto_monitor_*` 网页操作(与中控上线前相同)。
|
||||||
|
|
||||||
- 字段与各实例「手工开仓」一致:合约、方向、止盈止损模式(价格/百分比)、趋势单/波段单、杠杆、移动保本、止损/止盈。
|
|
||||||
- 提交后中控转发 `POST /api/hub/add_order`,逻辑与在实例网页下单相同(含以损定仓、门控等)。
|
|
||||||
- **Gate 趋势户** 同样可使用本 Tab(保留人工下单)。
|
|
||||||
|
|
||||||
#### Tab:关键位
|
|
||||||
|
|
||||||
- 仅 **币安、Gate 训练、OKX**(capabilities 含 `key`)显示。
|
|
||||||
- 类型:箱体突破、收敛突破、斐波回调、关键阻力/支撑等;模式含标准突破、箱体 1R、趋势单自填止盈。
|
|
||||||
- 提交转发 `POST /api/hub/add_key`。
|
|
||||||
- 页面上方会显示该实例 **关键位门控规则** 文案(来自 `/api/hub/meta`)。
|
|
||||||
|
|
||||||
#### Tab:趋势回调
|
|
||||||
|
|
||||||
- 仅 **Gate 趋势户**(capabilities 含 `trend`)。
|
|
||||||
- 流程:**填写参数 → 生成预览 → 确认执行(实盘)**。
|
|
||||||
- 预览转发 `POST /api/hub/trend/preview`;执行须带 `preview_id`,转发 `POST /api/hub/trend/execute`。
|
|
||||||
- 预览有效期内展示补仓档位表;过期需重新生成。
|
|
||||||
- 做空时「补仓上沿」在 UI 上提示为补仓下沿价(字段名仍为 `add_upper`,与实例一致)。
|
|
||||||
|
|
||||||
操作结果在页面底部 **Toast** 显示实例返回的 flash 汇总信息。
|
|
||||||
|
|
||||||
### 4.3 系统设置 `/settings`
|
|
||||||
|
|
||||||
**可用**:打开 http://127.0.0.1:5100/settings ,修改表格后点 **保存设置** 即写入 `hub_settings.json`;**重新加载** 从磁盘/默认再读(会重新套用 `HUB_DISABLED_IDS`)。保存后监控区、下单区立即使用新 URL/启用状态,**无需重启 hub**。
|
|
||||||
|
|
||||||
| 列 | 含义 |
|
| 列 | 含义 |
|
||||||
|----|------|
|
|----|------|
|
||||||
@@ -200,7 +174,7 @@ python hub.py
|
|||||||
| Flask URL | 实例根地址,如 `http://127.0.0.1:5001` |
|
| Flask URL | 实例根地址,如 `http://127.0.0.1:5001` |
|
||||||
| Agent URL | 子代理根地址,如 `http://127.0.0.1:15200` |
|
| Agent URL | 子代理根地址,如 `http://127.0.0.1:15200` |
|
||||||
| 复盘链接 | 一般为 `{Flask}/records` |
|
| 复盘链接 | 一般为 `{Flask}/records` |
|
||||||
| 能力 | 勾选 order / key / trend,控制下单区 Tab |
|
| 能力 | 勾选「监控关键位」「监控趋势计划」,控制监控卡片展示块 |
|
||||||
| id | 与 `HUB_DISABLED_IDS`、全平 API 路径中的 id 对应 |
|
| id | 与 `HUB_DISABLED_IDS`、全平 API 路径中的 id 对应 |
|
||||||
|
|
||||||
- **保存设置**:写入 `hub_settings.json`,重启 hub 后仍生效。
|
- **保存设置**:写入 `hub_settings.json`,重启 hub 后仍生效。
|
||||||
@@ -209,14 +183,14 @@ python hub.py
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 能力矩阵(下单区 Tab)
|
## 5. 能力矩阵(监控展示)
|
||||||
|
|
||||||
| 账户 | 人工下单 | 关键位 | 趋势回调 |
|
| 账户 | 监控关键位 | 监控趋势计划 |
|
||||||
|------|:--------:|:------:|:--------:|
|
|------|:----------:|:--------------:|
|
||||||
| 币安 | ✓ | ✓ | — |
|
| 币安 | ✓ | — |
|
||||||
| OKX | ✓ | ✓ | — |
|
| OKX | ✓ | — |
|
||||||
| Gate 训练 | ✓ | ✓ | — |
|
| Gate 训练 | ✓ | — |
|
||||||
| Gate 趋势 | ✓ | — | ✓ |
|
| Gate 趋势 | — | ✓ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -231,23 +205,13 @@ python hub.py
|
|||||||
| GET | `/api/monitor/board` | 监控聚合 |
|
| GET | `/api/monitor/board` | 监控聚合 |
|
||||||
| POST | `/api/close/{id}` | 单户全平 |
|
| POST | `/api/close/{id}` | 单户全平 |
|
||||||
| POST | `/api/close-all` | 全局全平,body 可选 `exclude_ids` |
|
| POST | `/api/close-all` | 全局全平,body 可选 `exclude_ids` |
|
||||||
| GET | `/api/trade/meta/{id}` | 实例 meta(门控文案等) |
|
|
||||||
| POST | `/api/trade/order/{id}` | 人工下单 |
|
|
||||||
| POST | `/api/trade/key/{id}` | 添加关键位 |
|
|
||||||
| POST | `/api/trade/trend/preview/{id}` | 趋势预览 |
|
|
||||||
| POST | `/api/trade/trend/execute/{id}` | 趋势执行 |
|
|
||||||
|
|
||||||
实例侧(需 `X-Hub-Token` 或已登录):
|
实例侧(中控只读调用 `/api/hub/monitor` 等;下单请在实例网页):
|
||||||
|
|
||||||
| 路径 | 说明 |
|
| 路径 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `/api/hub/ping` | 连通与能力 |
|
| `/api/hub/ping` | 连通与能力 |
|
||||||
| `/api/hub/meta` | 表单规则、预览 TTL 等 |
|
|
||||||
| `/api/hub/monitor` | 关键位、机器人单、趋势计划 |
|
| `/api/hub/monitor` | 关键位、机器人单、趋势计划 |
|
||||||
| `/api/hub/add_order` | 人工开仓 |
|
|
||||||
| `/api/hub/add_key` | 添加关键位 |
|
|
||||||
| `/api/hub/trend/preview` | 趋势预览 |
|
|
||||||
| `/api/hub/trend/execute` | 趋势执行 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -282,7 +246,7 @@ python hub.py
|
|||||||
|
|
||||||
## 8. 安全与边界
|
## 8. 安全与边界
|
||||||
|
|
||||||
1. **中控可下单**:下单区操作会真实调用交易所逻辑,与在实例网页操作等价,请确认账户与参数。
|
1. **中控不下单**:开仓、关键位、趋势回调仅在各实例网页操作。
|
||||||
2. **全平为市价减仓**:监控区全平不可撤销,操作前二次确认。
|
2. **全平为市价减仓**:监控区全平不可撤销,操作前二次确认。
|
||||||
3. **子代理建议只监听 127.0.0.1**,不要对局域网暴露 API Key 通道。
|
3. **子代理建议只监听 127.0.0.1**,不要对局域网暴露 API Key 通道。
|
||||||
4. **公网暴露 hub**:请防火墙限制 `5100`,或 `HUB_HOST=127.0.0.1` + `HUB_TRUST_LAN=0`。
|
4. **公网暴露 hub**:请防火墙限制 `5100`,或 `HUB_HOST=127.0.0.1` + `HUB_TRUST_LAN=0`。
|
||||||
@@ -297,11 +261,9 @@ python hub.py
|
|||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| 监控卡片「子代理不可用」 | agent 未启动或端口错 | 检查 Agent URL、启动 agent |
|
| 监控卡片「子代理不可用」 | agent 未启动或端口错 | 检查 Agent URL、启动 agent |
|
||||||
| 无关键位/趋势信息 | Flask 未启动或令牌错误 | 启动 app.py;核对 `HUB_BRIDGE_TOKEN` |
|
| 无关键位/趋势信息 | Flask 未启动或令牌错误 | 启动 app.py;核对 `HUB_BRIDGE_TOKEN` |
|
||||||
| 下单返回 401 | 令牌不一致 | 四实例与中控设相同 `HUB_BRIDGE_TOKEN` |
|
|
||||||
| 全平 401 | 子代理 `CONTROL_TOKEN` 与中控不一致 | 中控用 `X-Control-Token` 转发,需与 agent 一致 |
|
| 全平 401 | 子代理 `CONTROL_TOKEN` 与中控不一致 | 中控用 `X-Control-Token` 转发,需与 agent 一致 |
|
||||||
| OKX 始终灰色 | `HUB_DISABLED_IDS=1` | 清空该环境变量并在设置页启用 |
|
| OKX 始终灰色 | `HUB_DISABLED_IDS=1` | 清空该环境变量并在设置页启用 |
|
||||||
| 趋势预览成功但执行失败 | 预览过期 | 重新「生成预览」再执行 |
|
| 无关键位块 | 该户 capabilities 无 `key` | 正常;Gate 趋势户无关键位 |
|
||||||
| 关键位 Tab 灰色 | 该户 capabilities 无 `key` | 正常;Gate 趋势户无关键位 |
|
|
||||||
| 局域网无法打开中控 | 防火墙 / `HUB_TRUST_LAN=0` | 放行端口或恢复默认信任私网 |
|
| 局域网无法打开中控 | 防火墙 / `HUB_TRUST_LAN=0` | 放行端口或恢复默认信任私网 |
|
||||||
|
|
||||||
手动探测实例桥接:
|
手动探测实例桥接:
|
||||||
@@ -318,8 +280,8 @@ Invoke-WebRequest -Uri "http://127.0.0.1:5001/api/hub/ping" -Headers @{"X-Hub-To
|
|||||||
早期中控 **仅监控 + 全平**,使用环境变量 `HUB_AGENTS` 列表。当前版本改为:
|
早期中控 **仅监控 + 全平**,使用环境变量 `HUB_AGENTS` 列表。当前版本改为:
|
||||||
|
|
||||||
- **hub_settings.json**(或内置默认)管理四所 URL 与能力;
|
- **hub_settings.json**(或内置默认)管理四所 URL 与能力;
|
||||||
- **三页 UI**:监控 / 下单 / 设置;
|
- **两页 UI**:监控 / 设置;
|
||||||
- 通过 **hub_bridge** 调用实例下单 API。
|
- 通过 **hub_bridge** 只读聚合监控数据。
|
||||||
|
|
||||||
子代理 `agent.py` 仍负责持仓与全平;`HUB_AGENTS` 环境变量在新版 hub 中 **不再使用**(以设置文件为准)。
|
子代理 `agent.py` 仍负责持仓与全平;`HUB_AGENTS` 环境变量在新版 hub 中 **不再使用**(以设置文件为准)。
|
||||||
|
|
||||||
@@ -345,8 +307,8 @@ pm2 save && pm2 startup
|
|||||||
|
|
||||||
1. 启动四所 **agent** + **Flask**(OKX 按需)。
|
1. 启动四所 **agent** + **Flask**(OKX 按需)。
|
||||||
2. 启动 **hub.py**,打开监控区确认持仓与关键位门控正常。
|
2. 启动 **hub.py**,打开监控区确认持仓与关键位门控正常。
|
||||||
3. 在下单区选账户 → 人工单 / 关键位 / 趋势(按能力)。
|
3. 开仓、关键位、趋势 → 点击监控卡片「实例」进入对应 Flask。
|
||||||
4. 复盘、导出记录 → 点击监控卡片「复盘」进入对应实例。
|
4. 复盘、导出记录 → 点击「复盘」进入 `/records`。
|
||||||
5. 异常行情 → 单户全平或全局紧急全平。
|
5. 异常行情 → 单户全平或全局紧急全平。
|
||||||
|
|
||||||
如有新交易所,在 **系统设置** 添加一行并勾选能力,无需修改 hub.py 源码(需该所有 Flask 注册 `hub_bridge` 且 agent 已部署)。
|
如有新交易所,在 **系统设置** 添加一行并勾选能力,无需修改 hub.py 源码(需该所有 Flask 注册 `hub_bridge` 且 agent 已部署)。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 多账户交易中控 — 部署文档(含 PM2)
|
# 多账户交易中控 — 部署文档(含 PM2)
|
||||||
|
|
||||||
本文档说明在 **Ubuntu / Linux** 上部署 **manual_trading_hub**(监控区、下单区、系统设置)的推荐步骤。功能与界面操作见 **《使用说明.md》**;环境变量说明见 **`.env.example`** 与各 `crypto_monitor_*` 的 `.env.example`。
|
本文档说明在 **Ubuntu / Linux** 上部署 **manual_trading_hub**(监控区、系统设置)的推荐步骤。功能与界面操作见 **《使用说明.md》**;环境变量说明见 **`.env.example`** 与各 `crypto_monitor_*` 的 `.env.example`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ bash scripts/run_hub.sh
|
|||||||
1. 打开 **http://127.0.0.1:5100/monitor**(局域网用本机私网 IP)。
|
1. 打开 **http://127.0.0.1:5100/monitor**(局域网用本机私网 IP)。
|
||||||
2. 已启用账户应显示持仓;Flask 已起时有关键位/趋势信息。
|
2. 已启用账户应显示持仓;Flask 已起时有关键位/趋势信息。
|
||||||
3. **http://127.0.0.1:5100/settings** 保存后生成 `hub_settings.json`。
|
3. **http://127.0.0.1:5100/settings** 保存后生成 `hub_settings.json`。
|
||||||
4. **http://127.0.0.1:5100/trade** 选账户测试下单(实盘慎用)。
|
4. 在各实例 Flask 网页测试下单/关键位(中控仅监控,不下单)。
|
||||||
|
|
||||||
接口探测:
|
接口探测:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user