fix(hub): merge strategy snapshots into archive for gate_bot
Include strategy_trade_snapshots when trade_records is empty, harden SQL for older schemas, and show per-exchange sync errors in the archive UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -95,6 +95,7 @@ DIR = Path(__file__).resolve().parent
|
||||
HUB_BUILD = "20260607-hub-archive"
|
||||
_archive_sync_stop: asyncio.Event | None = None
|
||||
_archive_sync_task: asyncio.Task | None = None
|
||||
_last_archive_sync: dict | None = None
|
||||
HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8"))
|
||||
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
|
||||
HUB_BOARD_TIMEOUT = float(os.getenv("HUB_BOARD_TIMEOUT", "45"))
|
||||
@@ -239,6 +240,7 @@ def _schedule_board_refresh() -> None:
|
||||
|
||||
|
||||
async def _run_archive_sync_once() -> dict:
|
||||
global _last_archive_sync
|
||||
init_archive_db()
|
||||
settings = load_settings()
|
||||
targets = enabled_exchanges(settings)
|
||||
@@ -254,11 +256,25 @@ async def _run_archive_sync_once() -> dict:
|
||||
limit=ARCHIVE_TRADE_LIMIT,
|
||||
)
|
||||
if not trades_resp.get("ok"):
|
||||
st = trades_resp.get("status")
|
||||
msg = (
|
||||
trades_resp.get("msg")
|
||||
or trades_resp.get("error")
|
||||
or trades_resp.get("detail")
|
||||
or "拉取交易失败"
|
||||
)
|
||||
if st == 404:
|
||||
msg = (
|
||||
"HTTP 404:该 Flask 未注册 /api/hub/trades/archive。"
|
||||
"请在仓库根目录 git pull 后 pm2 restart crypto_gate crypto_gate_bot"
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"exchange_key": ex_key,
|
||||
"name": ex.get("name"),
|
||||
"ok": False,
|
||||
"msg": trades_resp.get("msg") or trades_resp.get("error") or "拉取交易失败",
|
||||
"status": st,
|
||||
"msg": msg,
|
||||
}
|
||||
)
|
||||
continue
|
||||
@@ -282,8 +298,17 @@ async def _run_archive_sync_once() -> dict:
|
||||
trades,
|
||||
remote_fetch,
|
||||
)
|
||||
r["name"] = ex.get("name")
|
||||
r["trade_count"] = len(trades)
|
||||
results.append(r)
|
||||
return {"ok": True, "exchanges": len(targets), "results": results}
|
||||
out = {
|
||||
"ok": True,
|
||||
"exchanges": len(targets),
|
||||
"results": results,
|
||||
"updated_at": __import__("datetime").datetime.now().isoformat(timespec="seconds"),
|
||||
}
|
||||
_last_archive_sync = out
|
||||
return out
|
||||
|
||||
|
||||
async def _archive_sync_loop() -> None:
|
||||
@@ -549,9 +574,13 @@ def _fetch_instance_trades_archive_sync(
|
||||
if r.status_code >= 400:
|
||||
parsed = _parse_http_json_body(r)
|
||||
parsed.setdefault("ok", False)
|
||||
parsed.setdefault("status", r.status_code)
|
||||
return parsed
|
||||
data = r.json() if r.content else {}
|
||||
return data if isinstance(data, dict) else {"ok": False, "msg": "无效 JSON"}
|
||||
if isinstance(data, dict):
|
||||
data.setdefault("ok", True)
|
||||
return data
|
||||
return {"ok": False, "msg": "无效 JSON"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)}
|
||||
|
||||
@@ -1662,6 +1691,7 @@ def api_archive_meta():
|
||||
"sync_interval_sec": ARCHIVE_SYNC_INTERVAL_SEC,
|
||||
"visible_bars_default": ARCHIVE_VISIBLE_BARS_DEFAULT,
|
||||
"exchanges": exchanges,
|
||||
"last_sync": _last_archive_sync,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -410,6 +410,9 @@
|
||||
const r = await apiFetch("/api/archive/meta");
|
||||
meta = await r.json();
|
||||
timeframe = (meta && meta.default_timeframe) || "15m";
|
||||
if (meta && meta.last_sync && elStatus && !elStatus.textContent) {
|
||||
setStatus(formatSyncSummary(meta.last_sync));
|
||||
}
|
||||
renderExchangeOptions();
|
||||
if (elTfTabs) {
|
||||
elTfTabs.querySelectorAll(".archive-tf-btn").forEach(function (btn) {
|
||||
@@ -418,16 +421,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatSyncSummary(j) {
|
||||
const results = j.results || [];
|
||||
const okN = results.filter(function (x) {
|
||||
return x.ok !== false;
|
||||
}).length;
|
||||
const parts = ["同步完成 · " + okN + "/" + (j.exchanges || 0) + " 所"];
|
||||
results.forEach(function (row) {
|
||||
const label = row.exchange_key || row.name || "?";
|
||||
if (row.ok === false) {
|
||||
parts.push(label + " 失败: " + (row.msg || "未知错误"));
|
||||
} else {
|
||||
parts.push(label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔");
|
||||
}
|
||||
});
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
async function syncAll() {
|
||||
setStatus("同步中(可能需数分钟)…");
|
||||
elBtnSync && (elBtnSync.disabled = true);
|
||||
try {
|
||||
const r = await apiFetch("/api/archive/sync", { method: "POST" });
|
||||
const j = await r.json();
|
||||
const okN = (j.results || []).filter(function (x) {
|
||||
return x.ok !== false;
|
||||
}).length;
|
||||
setStatus("同步完成 · " + okN + "/" + (j.exchanges || 0) + " 所");
|
||||
setStatus(formatSyncSummary(j));
|
||||
await loadList();
|
||||
if (selected) await openDetail(selected.exchange_key, selected.symbol);
|
||||
} catch (e) {
|
||||
|
||||
@@ -349,7 +349,7 @@
|
||||
<div id="toast"></div>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
|
||||
<script src="/assets/archive.js?v=20260607-hub-archive-v1"></script>
|
||||
<script src="/assets/archive.js?v=20260607-hub-archive-v2"></script>
|
||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user