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 = `
| 市场 | 比赛时间 | PP / 小时 | 下次调整 | 参考价 |
${state.markets.slice(0, 5).map((market) => `
| ${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))} |
`).join("")}
`;
}
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 = `
| Market ID | 市场名称 | 原因 | 更新时间 |
${state.disabledMarkets.map((row) => `
| ${escapeHtml(row.marketId)} |
${escapeHtml(row.title || `Market ${row.marketId}`)} |
${escapeHtml(row.reason || "--")} |
${escapeHtml(runtimeTime(row.updatedAt))} |
`).join("")}
`;
}
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]) => `| ${escapeHtml(label)} | `).join("")}
${rows.map((row) => `
${columns.map(([label, getter]) => {
const value = getter(row);
if (label === "方向") {
const side = String(value ?? "--");
return `| ${escapeHtml(side)} | `;
}
return `${escapeHtml(value ?? "--")} | `;
}).join("")}
`).join("")}
`;
}
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);