const state = { status: null, markets: [], orders: [], positions: [], selectedMarket: null, selectedMarketDetail: null, activeView: "overview", marketCursor: null, categoryTimings: {}, marketStats: {}, marketTokens: [], orderbook: null, bookOutcomeIndex: 0, strategyScan: null, strategyRuntime: null, disabledMarkets: [], }; const viewTitles = { overview: "账户总览", markets: "市场交易", positions: "持仓管理", orders: "订单管理", risk: "授权与风控", strategy: "策略控制", logs: "运行日志", settings: "系统配置", }; const MIN_PP_RATE = 60; const $ = (id) => document.getElementById(id); const text = (id, value) => { const node = $(id); if (node) node.textContent = value ?? "--"; }; function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function asArray(payload) { if (Array.isArray(payload)) return payload; if (Array.isArray(payload?.data)) return payload.data; if (Array.isArray(payload?.result?.data)) return payload.result.data; return []; } function formatTime(value = new Date()) { const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) return String(value || "--"); return date.toLocaleString("zh-CN", { hour12: false }); } function formatCompactTime(value) { if (!value) return "未提供"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }); } function formatUsd(value) { const number = Number(value); if (!Number.isFinite(number)) return "--"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(number); } function formatQuantity(value) { const number = Number(value); if (!Number.isFinite(number)) return "--"; return new Intl.NumberFormat("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 2, }).format(number); } function shortAddress(value) { if (!value) return "--"; const string = String(value); return string.length > 20 ? `${string.slice(0, 10)}...${string.slice(-8)}` : string; } function log(message, value, level = "info") { const output = $("logOut"); const prefix = level === "error" ? "ERROR" : level.toUpperCase(); const extra = value === undefined ? "" : `\n${JSON.stringify(value, null, 2)}`; const current = output.textContent === "控制台已就绪" ? "" : output.textContent; output.textContent = `[${new Date().toLocaleTimeString("zh-CN", { hour12: false })}] ${prefix} ${message}${extra}\n\n${current}`; } async function api(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options, }); const data = await response.json(); if (!data.ok) throw new Error(data.error || `HTTP ${response.status}`); return data.result; } function setView(name) { state.activeView = name; document.querySelectorAll(".navItem").forEach((item) => { item.classList.toggle("active", item.dataset.view === name); }); document.querySelectorAll(".view").forEach((panel) => { panel.classList.toggle("active", panel.dataset.viewPanel === name); }); text("pageTitle", viewTitles[name] || "Predict Console"); $("sidebar").classList.remove("open"); } function configState(id, label, kind) { const node = $(id); node.textContent = label; node.className = kind === "ok" ? "okText" : kind === "bad" ? "badText" : "warnText"; } function renderStatus(status) { state.status = status; const address = status.address || "未配置钱包"; const walletReady = status.privateKeyLoaded; const sdkReady = !status.sdkImportError; const jwtReady = status.jwtLoaded; text("statusLine", `${shortAddress(status.address)} / ${status.apiBase} / ${status.chain}`); text("metricChain", String(status.chain || "--").toUpperCase()); text("metricAuth", jwtReady ? "已认证" : walletReady ? "待认证" : "未配置"); text("overviewAddress", address); text("overviewSigner", status.usingPredictAccount ? "Predict Account" : walletReady ? "EOA" : "--"); text("overviewApi", status.apiBase); text("overviewJwt", jwtReady ? "已加载" : "未加载"); text("riskChain", status.chain); text("riskWallet", address); text("settingsApi", status.apiBase); text("settingsChain", status.chain); text("settingsAddress", address); text("settingsApiKey", status.apiKeyLoaded ? "已加载" : "未加载"); text("settingsPrivateKey", walletReady ? "已加载" : "未加载"); text("settingsSdk", sdkReady ? "正常" : status.sdkImportError); configState("configApi", "已连接", "ok"); configState("configJwt", jwtReady ? "已配置" : "未认证", jwtReady ? "ok" : "warn"); configState("configKey", walletReady ? "已配置" : "未配置", walletReady ? "ok" : "bad"); configState("configSdk", sdkReady ? "正常" : "加载失败", sdkReady ? "ok" : "bad"); configState("configChain", String(status.chain || "--").toUpperCase(), "ok"); text("sidebarStatus", walletReady ? "钱包已加载" : "仅公开数据"); text("sidebarNetwork", String(status.chain || "Predict API").toUpperCase()); $("sidebarDot").classList.toggle("error", !sdkReady); if (state.strategyRuntime) renderStrategyRuntime(state.strategyRuntime); } async function loadStatus() { const status = await api("/api/status"); renderStatus(status); return status; } function marketTitle(market) { return market.title || market.question || market.name || `Market ${market.id}`; } function marketMeta(market) { return [ `ID ${market.id}`, market.tradingStatus || market.status || market.state, market.category || market.categorySlug, ].filter(Boolean).join(" / "); } function marketTiming(market) { const category = state.categoryTimings[market.categorySlug] || {}; return { startsAt: market.startsAt || category.startsAt || null, endsAt: market.endsAt || category.endsAt || null, }; } function marketDisplayTime(market) { const timing = marketTiming(market); const startsAt = timing.startsAt ? new Date(timing.startsAt).getTime() : null; if (startsAt && Date.now() < startsAt) { return { label: "开始", value: timing.startsAt }; } if (timing.endsAt) { return { label: "结束", value: timing.endsAt }; } if (timing.startsAt) { return { label: "开始", value: timing.startsAt }; } return { label: "时间", value: null }; } function currentReward(market) { return market.rewards?.current || null; } function nextRewardChange(market) { const currentRate = Number(currentReward(market)?.hourlyRate || 0); const currentEndsAt = currentReward(market)?.endsAt || null; const schedule = market.rewards?.schedule || []; const next = schedule .filter((period) => new Date(period.startsAt).getTime() > Date.now()) .sort((a, b) => new Date(a.startsAt) - new Date(b.startsAt))[0]; if (!next) { const endsAtMs = currentEndsAt ? new Date(currentEndsAt).getTime() : NaN; if (Number.isFinite(endsAtMs) && endsAtMs > Date.now()) { return { direction: "下调", startsAt: currentEndsAt, hourlyRate: 0, }; } return null; } const nextRate = Number(next.hourlyRate || 0); const direction = nextRate > currentRate ? "上调" : nextRate < currentRate ? "下调" : "调整"; return { direction, startsAt: next.startsAt, hourlyRate: nextRate, }; } function marketPrice(market) { const outcomes = market.outcomes || []; const candidate = outcomes.find((item) => item.bestAsk != null) || outcomes.find((item) => item.bestBid != null); const level = candidate?.bestAsk ?? candidate?.bestBid; const value = typeof level === "object" ? level?.price : level; return value == null ? "--" : `${(Number(value) * 100).toFixed(1)}c`; } function extractTokens(market) { const outcomes = market.outcomes || market.tokens || market.marketOutcomes || []; return outcomes.map((item, index) => ({ label: item.name || item.outcome || item.title || `Outcome ${index + 1}`, tokenId: String(item.tokenId || item.token_id || item.onChainId || item.on_chain_id || item.id || item.token || ""), indexSet: Number(item.indexSet || item.index_set || index + 1), })).filter((item) => item.tokenId); } function renderMarketSnapshot() { const container = $("marketSnapshot"); if (!state.markets.length) { container.className = "emptyState"; container.textContent = "没有可显示的市场"; return; } container.className = "tableWrap"; container.innerHTML = ` ${state.markets.slice(0, 5).map((market) => ` `).join("")}
市场比赛时间PP / 小时下次调整参考价
${escapeHtml(marketTitle(market))} ${escapeHtml(`${marketDisplayTime(market).label} ${formatCompactTime(marketDisplayTime(market).value)}`)} ${escapeHtml(currentReward(market)?.hourlyRate ?? 0)} ${escapeHtml(nextRewardChange(market) ? `${nextRewardChange(market).direction}至 ${nextRewardChange(market).hourlyRate} / ${formatCompactTime(nextRewardChange(market).startsAt)}` : "暂无计划")} ${escapeHtml(marketPrice(market))}
`; } function renderMarkets() { const list = $("marketsList"); list.innerHTML = ""; if (!state.markets.length) { list.innerHTML = '
未找到市场
'; return; } state.markets.forEach((market) => { const reward = currentReward(market); const rewardChange = nextRewardChange(market); const eventTime = marketDisplayTime(market); const stats = state.marketStats[market.id]; const button = document.createElement("button"); button.className = `marketItem${state.selectedMarket === market.id ? " active" : ""}`; button.innerHTML = `
${escapeHtml(marketTitle(market))} ${escapeHtml(marketMeta(market))}
${escapeHtml(eventTime.label)} ${escapeHtml(formatCompactTime(eventTime.value))} 流动性 ${escapeHtml(stats ? formatUsd(stats.totalLiquidityUsd) : "选择后加载")} 奖励至 ${escapeHtml(formatCompactTime(reward?.endsAt))} 调整 ${escapeHtml(rewardChange ? `${rewardChange.direction}至 ${rewardChange.hourlyRate} @ ${formatCompactTime(rewardChange.startsAt)}` : "暂无计划")}
${escapeHtml(marketPrice(market))}
${escapeHtml(reward?.hourlyRate ?? 0)} PP/h
`; button.addEventListener("click", () => selectMarket(market.id)); list.appendChild(button); }); } async function loadCategoryTimings(markets) { const slugs = [...new Set( markets .map((market) => market.categorySlug) .filter((slug) => slug && !Object.hasOwn(state.categoryTimings, slug)), )]; await Promise.all(slugs.map(async (slug) => { try { const category = await api(`/api/categories/${encodeURIComponent(slug)}`); state.categoryTimings[slug] = { startsAt: category.startsAt || null, endsAt: category.endsAt || null, }; } catch (error) { state.categoryTimings[slug] = null; log(`Category 时间加载失败: ${slug}`, error.message, "error"); } })); renderMarkets(); renderMarketSnapshot(); return state.categoryTimings; } async function loadMarkets(reset = true) { const params = new URLSearchParams(); if ($("search").value) params.set("search", $("search").value); if ($("category").value) params.set("category", $("category").value); if ($("status").value) params.set("status", $("status").value); params.set("hasActiveRewards", "true"); if (!reset && state.marketCursor) params.set("after", state.marketCursor); params.set("first", "100"); const result = await api(`/api/markets?${params.toString()}`); const rows = asArray(result).filter((market) => { const tradingStatus = String(market.tradingStatus || "").toUpperCase(); const rewardRate = Number(currentReward(market)?.hourlyRate || 0); return (!tradingStatus || tradingStatus === "OPEN") && rewardRate >= MIN_PP_RATE; }); if (reset) { state.markets = rows; } else { const existingIds = new Set(state.markets.map((market) => market.id)); state.markets = [...state.markets, ...rows.filter((market) => !existingIds.has(market.id))]; } state.marketCursor = result?.cursor || null; $("loadMoreMarkets").disabled = !state.marketCursor; $("loadMoreMarkets").textContent = state.marketCursor ? "加载更多" : "已经到底"; renderMarkets(); renderMarketSnapshot(); await loadCategoryTimings(rows); log(`${reset ? "已加载" : "已追加"} ${rows.length} 个市场`, { total: state.markets.length, nextCursor: state.marketCursor }); return state.markets; } function renderStrategyScan(scan) { state.strategyScan = scan; if (!scan) { text("metricMarkets", "-- / --"); text("marketCount", "尚未执行每小时市场筛选"); return; } const total = scan.totalMarketCount ?? "--"; const eligible = scan.eligibleMarketCount ?? 0; const scannedAt = scan.scannedAt ? new Date(scan.scannedAt).toLocaleString("zh-CN", { hour12: false }) : "--"; text("metricMarkets", `${eligible} / ${total}`); text( "marketCount", `最近筛选:总计 ${total} 个市场,符合条件 ${eligible} 个 · ${scannedAt}`, ); } async function loadStrategyScan() { const scan = await api("/api/strategy/scan"); renderStrategyScan(scan); return scan; } function runtimeTime(value) { return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "--"; } function renderStrategyRuntime(runtime) { state.strategyRuntime = runtime; const statusLabels = { STARTING: "启动中", RUNNING: "运行中", STOP_REQUESTED: "正在停止", STOPPING: "正在停止", STOPPED: "已停止", START_FAILED: "启动失败", EXITED: "进程已退出", }; const modeLabels = { LIVE: "实盘", DRY_RUN: "模拟", PUBLIC_SCAN: "公开扫描", }; const alive = Boolean(runtime?.processAlive); const walletReady = Boolean(state.status?.privateKeyLoaded); text("strategyRuntimeStatus", statusLabels[runtime?.status] || runtime?.status || "已停止"); text("strategyRuntimeMode", modeLabels[runtime?.mode] || runtime?.mode || "--"); text("strategyRuntimePid", runtime?.pid || "--"); text("strategyStartedAt", runtimeTime(runtime?.startedAt)); text("strategyLastCycleAt", runtimeTime(runtime?.lastCycleAt)); text( "strategyInterval", runtime?.intervalSeconds != null ? `${runtime.intervalSeconds} 秒` : "--", ); $("strategyRuntimeError").textContent = runtime?.lastError || (walletReady ? "无运行错误" : "未加载 PREDICT_PRIVATE_KEY"); $("startDryStrategy").disabled = alive || !walletReady; $("startLiveStrategy").disabled = alive || !walletReady; $("forceStrategyRescan").disabled = !alive; $("stopStrategy").disabled = !alive; } async function loadStrategyRuntime() { const runtime = await api("/api/strategy/runtime"); renderStrategyRuntime(runtime); return runtime; } async function startStrategy(mode) { if (mode === "LIVE" && !window.confirm("确认启动实盘策略?策略将提交和取消真实订单。")) { return; } const runtime = await api("/api/strategy/start", { method: "POST", body: JSON.stringify({ mode }), }); renderStrategyRuntime(runtime); log(mode === "LIVE" ? "实盘策略启动中" : "模拟策略启动中", runtime); window.setTimeout(() => loadStrategyRuntime().catch(() => {}), 1000); } async function stopStrategy() { const runtime = await api("/api/strategy/stop", { method: "POST" }); renderStrategyRuntime(runtime); log("策略停止请求已发送", runtime); window.setTimeout(() => loadStrategyRuntime().catch(() => {}), 1200); } async function forceStrategyRescan() { const runtime = await api("/api/strategy/rescan", { method: "POST" }); renderStrategyRuntime(runtime); log("已请求策略立即重筛市场,下一轮轮询会执行", runtime); window.setTimeout(() => { loadStrategyRuntime().catch(() => {}); loadStrategyScan().catch(() => {}); }, 5000); } function renderDisabledMarkets(rows) { state.disabledMarkets = rows || []; const out = $("disabledMarketsOut"); if (!out) return; if (!state.disabledMarkets.length) { out.innerHTML = `
没有禁用市场
`; return; } out.innerHTML = ` ${state.disabledMarkets.map((row) => ` `).join("")}
Market ID市场名称原因更新时间
${escapeHtml(row.marketId)} ${escapeHtml(row.title || `Market ${row.marketId}`)} ${escapeHtml(row.reason || "--")} ${escapeHtml(runtimeTime(row.updatedAt))}
`; } async function loadDisabledMarkets() { const rows = await api("/api/strategy/disabled-markets"); renderDisabledMarkets(rows); return rows; } function disabledMarketPayload() { const marketId = Number($("disabledMarketId")?.value || 0); if (!Number.isInteger(marketId) || marketId <= 0) { throw new Error("请输入有效的 Market ID"); } const market = state.markets.find((item) => Number(item.id) === marketId); const selected = Number(state.selectedMarketDetail?.id) === marketId ? state.selectedMarketDetail : null; return { marketId, title: selected || market ? marketTitle(selected || market) : "", reason: $("disabledMarketReason")?.value || "", }; } async function disableMarket() { const payload = disabledMarketPayload(); const result = await api("/api/strategy/disabled-markets/disable", { method: "POST", body: JSON.stringify(payload), }); renderDisabledMarkets(result.markets); log(`已禁用市场 ${payload.marketId},策略下一轮会停止新增订单并撤挂单`, result); return result; } async function enableMarket() { const payload = disabledMarketPayload(); const result = await api("/api/strategy/disabled-markets/enable", { method: "POST", body: JSON.stringify({ marketId: payload.marketId }), }); renderDisabledMarkets(result.markets); log(`已解禁市场 ${payload.marketId}`, result); return result; } async function selectMarket(marketId) { const detail = await api(`/api/markets/${marketId}`); state.selectedMarket = marketId; state.selectedMarketDetail = detail; const title = marketTitle(detail); const reward = currentReward(detail); const eventTime = marketDisplayTime(detail); text( "selectedMarket", `${title} / ${eventTime.label} ${formatCompactTime(eventTime.value)} / ${reward?.hourlyRate ?? 0} PP/h`, ); text("riskMarket", title); text("riskMarketId", marketId); ["statLiquidity", "statVolume24h", "statVolumeTotal", "statBidTotal", "statAskTotal", "statBookTotal"] .forEach((id) => text(id, "--")); const select = $("tokenSelect"); select.innerHTML = ""; state.marketTokens = extractTokens(detail); state.bookOutcomeIndex = 0; state.orderbook = null; state.marketTokens.forEach((token, index) => { const option = document.createElement("option"); option.value = token.tokenId; option.textContent = `${token.label} / ${shortAddress(token.tokenId)}`; option.dataset.outcomeIndex = String(index); select.appendChild(option); }); renderOutcomeSwitch(); renderMarkets(); log("已选择市场", { id: marketId, title }); await Promise.allSettled([ loadOrderbook().catch((error) => log("订单簿加载失败", error.message, "error")), loadMarketStats(marketId).catch((error) => log("市场统计加载失败", error.message, "error")), ]); } function bookNotional(rows) { if (!Array.isArray(rows)) return 0; return rows.reduce((total, item) => { const price = Number(Array.isArray(item) ? item[0] : item.price); const quantity = Number(Array.isArray(item) ? item[1] : item.quantity ?? item.size); return total + (Number.isFinite(price) && Number.isFinite(quantity) ? price * quantity : 0); }, 0); } function normalizedDepth(rows, side) { if (!Array.isArray(rows)) return []; return rows .map((item) => ({ price: Number(Array.isArray(item) ? item[0] : item.price), quantity: Number(Array.isArray(item) ? item[1] : item.quantity ?? item.size), })) .filter((item) => Number.isFinite(item.price) && Number.isFinite(item.quantity)) .sort((a, b) => side === "bid" ? b.price - a.price : a.price - b.price) .slice(0, 5); } function depthRows(rows, side) { const levels = normalizedDepth(rows, side); const prefix = side === "bid" ? "买" : "卖"; if (!levels.length) return '
暂无深度
'; let cumulativeAmount = 0; return levels.map((item, index) => { const amount = item.price * item.quantity; cumulativeAmount += amount; return `
${prefix}${index + 1} ${item.price} ${formatQuantity(item.quantity)} ${formatUsd(amount)} ${formatUsd(cumulativeAmount)}
`; }).join(""); } function renderOutcomeSwitch() { const container = $("bookOutcomeSwitch"); container.innerHTML = ""; state.marketTokens.forEach((token, index) => { const button = document.createElement("button"); button.type = "button"; button.textContent = token.label; button.classList.toggle("active", index === state.bookOutcomeIndex); button.addEventListener("click", () => { state.bookOutcomeIndex = index; $("tokenSelect").selectedIndex = index; renderOutcomeSwitch(); if (state.orderbook) renderOrderbook(state.orderbook); }); container.appendChild(button); }); } function bookForSelectedOutcome(book) { const token = state.marketTokens[state.bookOutcomeIndex]; const isPrimary = !token || token.indexSet === 1 || token.label.toUpperCase() === "YES"; const precision = Number(state.selectedMarketDetail?.decimalPrecision ?? 2); const complement = (rows) => (rows || []).map((item) => { const price = Number(Array.isArray(item) ? item[0] : item.price); const quantity = Number(Array.isArray(item) ? item[1] : item.quantity ?? item.size); return [Number((1 - price).toFixed(precision)), quantity]; }); const selected = isPrimary ? { ...book, bids: book.bids || [], asks: book.asks || [] } : { ...book, bids: complement(book.asks), asks: complement(book.bids), }; const bids = normalizedDepth(selected.bids, "bid"); const asks = normalizedDepth(selected.asks, "ask"); const tick = Number((10 ** -precision).toFixed(precision)); const maximumPrice = Number((1 - tick).toFixed(precision)); const samePrice = (left, right) => Math.abs(left - right) < Number.EPSILON * 10; if (!bids.length && asks.length && !samePrice(asks[0].price, tick)) { selected.bids = [[tick, 0]]; } if (!asks.length && bids.length && !samePrice(bids[0].price, maximumPrice)) { selected.asks = [[maximumPrice, 0]]; } return selected; } function renderOrderbook(payload) { const rawBook = payload?.data || payload || {}; state.orderbook = rawBook; const book = bookForSelectedOutcome(rawBook); const outcome = state.marketTokens[state.bookOutcomeIndex]; const bidTotal = bookNotional(normalizedDepth(book.bids, "bid")); const askTotal = bookNotional(normalizedDepth(book.asks, "ask")); $("orderbook").className = "bookView"; $("orderbook").innerHTML = `

买盘前五档

档位价格数量本档金额累计金额
${depthRows(book.bids, "bid")}

卖盘前五档

档位价格数量本档金额累计金额
${depthRows(book.asks, "ask")}
`; text("bookOutcomeLabel", outcome ? `/ ${outcome.label}` : ""); text("bookUpdated", rawBook.updateTimestampMs ? formatTime(rawBook.updateTimestampMs) : formatTime()); text("statBidTotal", formatUsd(bidTotal)); text("statAskTotal", formatUsd(askTotal)); text("statBookTotal", formatUsd(bidTotal + askTotal)); } async function loadOrderbook() { if (!state.selectedMarket) { throw new Error("请先选择市场"); } const book = await api(`/api/markets/${state.selectedMarket}/orderbook`); renderOrderbook(book); return book; } async function loadMarketStats(marketId) { const stats = await api(`/api/markets/${marketId}/stats`); state.marketStats[marketId] = stats; text("statLiquidity", formatUsd(stats.totalLiquidityUsd)); text("statVolume24h", formatUsd(stats.volume24hUsd)); text("statVolumeTotal", formatUsd(stats.volumeTotalUsd)); renderMarkets(); log("市场统计已加载", { marketId, stats }); return stats; } function orderRows(payload) { return asArray(payload); } function positionRows(payload) { return asArray(payload); } function renderOrders() { const rows = state.orders; text("metricOrders", rows.length); const columns = [ ["订单 ID", (x) => x.id || x.orderId || x.order?.hash], ["市场", (x) => x.market?.question || x.marketId || x.order?.tokenId], ["方向", (x) => x.side ?? x.order?.side], ["价格", (x) => x.pricePerShare || x.price], ["数量", (x) => x.amount || x.quantity || x.order?.makerAmount], ["状态", (x) => x.status || "OPEN"], ]; renderTable("ordersOut", rows, columns); renderTable("recentOrders", rows.slice(0, 5), columns.slice(0, 4), "暂无开放订单"); } function renderPositions() { const rows = state.positions; text("metricPositions", rows.length); const positionQuantity = (position) => { const raw = position.quantity ?? position.balance ?? position.amount; const numeric = Number(raw); if (!Number.isFinite(numeric)) return raw; return formatQuantity(Math.abs(numeric) >= 1e12 ? numeric / 1e18 : numeric); }; const columns = [ ["市场", (x) => x.market?.question || x.marketTitle || x.marketId], ["Outcome", (x) => x.outcome?.name || x.outcome || x.tokenId], ["数量", positionQuantity], ["均价", (x) => x.averagePrice || x.avgPrice || x.averageBuyPriceUsd], ["现价", (x) => x.currentPrice || x.price], ["价值", (x) => x.value || x.currentValue || x.valueUsd], ]; renderTable("positionsOut", rows, columns, "暂无开放持仓"); } function renderTable(containerId, rows, columns, emptyText = "暂无数据") { const container = $(containerId); if (!rows.length) { container.className = "emptyState"; container.textContent = emptyText; return; } container.className = "tableWrap"; container.innerHTML = ` ${columns.map(([label]) => ``).join("")}${rows.map((row) => ` ${columns.map(([label, getter]) => { const value = getter(row); if (label === "方向") { const side = String(value ?? "--"); return ``; } return ``; }).join("")}`).join("")}
${escapeHtml(label)}
${escapeHtml(side)}${escapeHtml(value ?? "--")}
`; } async function loadOrders() { const result = await api("/api/orders?status=OPEN&first=100"); state.orders = orderRows(result); renderOrders(); log(`已加载 ${state.orders.length} 个开放订单`); return result; } async function loadPositions() { const result = await api("/api/positions?first=100"); state.positions = positionRows(result); renderPositions(); log(`已加载 ${state.positions.length} 个持仓`); return result; } async function loadAccount() { const [accountResult, balanceResult] = await Promise.allSettled([ api("/api/account"), api("/api/balance"), ]); const account = accountResult.status === "fulfilled" ? accountResult.value : null; const balance = balanceResult.status === "fulfilled" ? balanceResult.value : null; const amount = balance?.usdt ?? balance?.data?.usdt ?? "--"; text("metricBalance", amount); text("overviewBalance", `${amount} USDT`); text("overviewAddress", account?.data?.address || account?.address || state.status?.address); if (accountResult.status === "rejected") { log("account load failed; balance synced separately", accountResult.reason.message, "error"); } if (balanceResult.status === "rejected") { log("balance load failed", balanceResult.reason.message, "error"); } log("账户数据已同步", { account, balance }); return { account, balance }; } async function refreshOverview() { const status = await loadStatus(); const tasks = [ loadMarkets(true), loadStrategyScan().catch((error) => { renderStrategyScan(null); log("策略筛选统计加载失败", error.message, "error"); }), ]; if (status.privateKeyLoaded) { tasks.push( loadAccount().catch((error) => log("账户数据加载失败", error.message, "error")), loadOrders().catch((error) => log("订单加载失败", error.message, "error")), loadPositions().catch((error) => log("持仓加载失败", error.message, "error")), ); } else { text("metricBalance", "--"); text("metricOrders", "--"); text("metricPositions", "--"); } await Promise.allSettled(tasks); const now = formatTime(); text("metricSync", now); text("overviewSync", now); log("总览数据同步完成"); } async function submitOrder() { if (!state.selectedMarket) throw new Error("请先选择市场"); if (!$("tokenSelect").value) throw new Error("当前市场没有可交易 Outcome"); const quantity = $("quantity").value.trim(); if (!/^\d+(\.\d{1,2})?$/.test(quantity)) { throw new Error("份额数量最多只能保留两位小数"); } const payload = { marketId: state.selectedMarket, tokenId: $("tokenSelect").value, side: $("side").value, strategy: $("strategy").value, price: $("price").value, quantity, value: $("value").value, slippageBps: $("slippageBps").value, isPostOnly: $("isPostOnly").checked, }; const result = await api("/api/orders", { method: "POST", body: JSON.stringify(payload) }); log("订单提交成功", result); await loadOrders(); return result; } function idsFromInput() { return $("cancelIds").value.split(",").map((value) => value.trim()).filter(Boolean); } async function approvalAction(run) { if (!state.selectedMarket) throw new Error("请先在市场交易页选择市场"); const path = run ? `/api/approvals/${state.selectedMarket}/run` : `/api/approvals/${state.selectedMarket}`; const result = await api(path, run ? { method: "POST" } : {}); $("riskOutput").textContent = JSON.stringify(result, null, 2); log(run ? "授权执行完成" : "授权检查完成", result); return result; } async function strategyApprovalAction(run) { if (run) { const runtime = await loadStrategyRuntime(); if (runtime.processAlive) { throw new Error("请先在策略控制页停止策略"); } const confirmed = window.confirm( "确认执行策略市场缺失授权?这会发送链上交易并可能消耗 BNB Gas。", ); if (!confirmed) return; } const result = await api( run ? "/api/strategy/approvals/run" : "/api/strategy/approvals", run ? { method: "POST", body: JSON.stringify({ confirm: "AUTHORIZE_STRATEGY_MARKETS" }), } : {}, ); $("strategyApprovalsOutput").textContent = JSON.stringify(result, null, 2); const message = run ? (result.success ? "策略市场授权执行成功" : "策略市场授权执行失败") : "策略市场授权检查完成"; log(message, result, run && !result.success ? "error" : "info"); return result; } function wire(id, handler) { const node = $(id); if (!node) return; node.addEventListener("click", async () => { node.disabled = true; try { await handler(); } catch (error) { log(`${node.textContent.trim()}失败`, error.message, "error"); } finally { node.disabled = false; } }); } document.querySelectorAll(".navItem").forEach((item) => { item.addEventListener("click", () => setView(item.dataset.view)); }); document.querySelectorAll(".navJump").forEach((item) => { item.addEventListener("click", () => setView(item.dataset.target)); }); $("menuButton").addEventListener("click", () => $("sidebar").classList.toggle("open")); $("strategy").addEventListener("change", () => { const market = $("strategy").value === "MARKET"; $("price").disabled = market; $("value").disabled = !market; $("isPostOnly").disabled = market; }); $("tokenSelect").addEventListener("change", () => { state.bookOutcomeIndex = Math.max($("tokenSelect").selectedIndex, 0); renderOutcomeSwitch(); if (state.orderbook) renderOrderbook(state.orderbook); }); $("search").addEventListener("keydown", (event) => { if (event.key === "Enter") loadMarkets().catch((error) => log("市场搜索失败", error.message, "error")); }); wire("refreshAll", refreshOverview); wire("settingsRefresh", loadStatus); wire("refreshAccount", loadAccount); wire("refreshAuth", async () => { await api("/api/auth/refresh", { method: "POST" }); await loadStatus(); log("Predict JWT 已刷新"); }); wire("loadMarkets", loadMarkets); wire("loadMoreMarkets", () => loadMarkets(false)); wire("loadOrderbook", loadOrderbook); wire("submitOrder", submitOrder); wire("loadOrders", loadOrders); wire("loadPositions", loadPositions); wire("checkApprovals", () => approvalAction(false)); wire("runApprovals", () => approvalAction(true)); wire("riskCheckApprovals", () => approvalAction(false)); wire("riskRunApprovals", () => approvalAction(true)); wire("checkStrategyApprovals", () => strategyApprovalAction(false)); wire("runStrategyApprovals", () => strategyApprovalAction(true)); wire("refreshStrategy", loadStrategyRuntime); wire("startDryStrategy", () => startStrategy("DRY_RUN")); wire("startLiveStrategy", () => startStrategy("LIVE")); wire("forceStrategyRescan", forceStrategyRescan); wire("stopStrategy", stopStrategy); wire("refreshDisabledMarkets", loadDisabledMarkets); wire("disableMarket", disableMarket); wire("enableMarket", enableMarket); wire("removeOrders", async () => { const ids = idsFromInput(); if (!ids.length) throw new Error("请输入订单 ID"); const result = await api("/api/orders/remove", { method: "POST", body: JSON.stringify({ ids }) }); log("订单已从订单簿移除", result); await loadOrders(); }); wire("cancelOrders", async () => { const ids = idsFromInput(); if (!ids.length) throw new Error("请输入订单 ID"); const result = await api("/api/orders/cancel", { method: "POST", body: JSON.stringify({ ids }) }); log("链上取消完成", result); await loadOrders(); }); wire("clearLog", async () => { $("logOut").textContent = "控制台已清空"; }); $("rewardFilter").addEventListener("change", () => { loadMarkets(true).catch((error) => log("奖励筛选失败", error.message, "error")); }); $("status").addEventListener("change", () => { loadMarkets(true).catch((error) => log("状态筛选失败", error.message, "error")); }); $("strategy").dispatchEvent(new Event("change")); const initialView = new URLSearchParams(window.location.search).get("view"); if (initialView && viewTitles[initialView]) setView(initialView); refreshOverview().catch((error) => log("初始化失败", error.message, "error")); loadStrategyRuntime().catch((error) => log("策略状态加载失败", error.message, "error")); loadDisabledMarkets().catch((error) => log("禁用市场加载失败", error.message, "error")); window.setInterval(() => { loadStrategyRuntime().catch(() => {}); }, 3000);