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, strategyRuntimes: {}, disabledMarkets: [], strategyWallets: [], }; const viewTitles = { overview: "账户总览", markets: "市场交易", positions: "持仓管理", orders: "订单管理", risk: "授权与风控", strategy: "策略控制", pp_estimate: "PP 预估", settlement_pair_dry: "对冲 DRY", logs: "运行日志", settings: "系统配置", }; const MIN_PP_RATE = 60; viewTitles.pricing_deviation = "定价偏差"; const MANAGE_ALL_OPEN_ORDERS_KEY = "predict.manageAllOpenOrders"; const ACCOUNT_WALLET_KEY = "predict.accountWallet"; const STRATEGY_TYPE_KEY = "predict.strategyType"; const STRATEGY_WALLET_KEY = "predict.strategyWallet"; const RISK_WALLET_KEY = "predict.riskWallet"; const PP_ESTIMATE_WALLET_KEY = "predict.ppEstimateWallet"; const STRATEGY_MIN_PP_RATE_KEY = "predict.strategyMinPpRate"; const $ = (id) => document.getElementById(id); const text = (id, value) => { const node = $(id); if (node) node.textContent = value ?? "--"; }; function manageAllOpenOrdersEnabled() { return Boolean($("manageAllOpenOrders")?.checked); } function persistManageAllOpenOrders() { localStorage.setItem( MANAGE_ALL_OPEN_ORDERS_KEY, manageAllOpenOrdersEnabled() ? "1" : "0", ); } function selectedStrategyType() { return $("strategyType")?.value || "dynamic"; } function defaultStrategyMinPpRate(strategyType = selectedStrategyType()) { if (strategyType === "poly_price") return "20"; if (strategyType === "poly_settlement_pair") return "1"; if (strategyType === "pp_score_value") return "0.15"; return strategyType === "pp_value" ? "1" : "60"; } function strategyMinPpRateLabel(strategyType = selectedStrategyType()) { if (strategyType === "btc_five_minute_oco" || strategyType === "BTC_FIVE_MINUTE_OCO") return "每边投入 USDT"; if (strategyType === "pp_score_value" || strategyType === "PP_SCORE_VALUE") return "最低价值分"; if (strategyType === "poly_price" || strategyType === "POLY_PRICE") return "固定买入上限 USDT"; if (strategyType === "poly_settlement_pair" || strategyType === "POLY_SETTLEMENT_PAIR") return "最低偏差比例 %"; return strategyType === "pp_value" || strategyType === "PP_VALUE" ? "最低可得 PP/h" : "最低市场 PP/h"; } function syncStrategyMinPpLabels(strategyType = selectedStrategyType()) { text("strategyMinPpRateLabel", strategyMinPpRateLabel(strategyType)); text("strategyMinPpRateInputLabel", strategyMinPpRateLabel(selectedStrategyType())); const btc = selectedStrategyType() === "btc_five_minute_oco"; $("strategyMinPpRateInput").parentElement.hidden = btc; $("btcOrderBudgetControl").hidden = !btc; $("btcFutureMarketsControl").hidden = !btc; $("btcOcoDetails").hidden = !btc; $("manageAllOpenOrders").parentElement.hidden = btc; } function selectedAccountWalletId() { return $("accountWalletSelect")?.value || "default"; } function selectedStrategyWalletId() { return $("strategyWalletSelect")?.value || localStorage.getItem(STRATEGY_WALLET_KEY) || "default"; } function selectedRiskWalletId() { return $("riskWalletSelect")?.value || selectedAccountWalletId(); } function selectedPPEstimateWalletId() { return $("ppEstimateWalletSelect")?.value || selectedAccountWalletId(); } function selectedStrategyMinPpRate() { const value = String($("strategyMinPpRateInput")?.value || defaultStrategyMinPpRate()).trim(); return value || defaultStrategyMinPpRate(); } function selectedStrategyWalletReady() { const walletId = selectedStrategyWalletId(); const wallet = state.strategyWallets.find((item) => item.id === walletId); return Boolean(wallet?.ready); } function selectedAccountWalletReady() { const walletId = selectedAccountWalletId(); const wallet = state.strategyWallets.find((item) => item.id === walletId); return Boolean(wallet?.ready); } function selectedAccountWallet() { return state.strategyWallets.find((item) => item.id === selectedAccountWalletId()) || null; } function selectedStrategyWallet() { return state.strategyWallets.find((item) => item.id === selectedStrategyWalletId()) || null; } function walletDisplay(wallet) { if (!wallet) return "--"; return `${wallet.label || wallet.id} / ${shortAddress(wallet.accountAddress || wallet.address) || "--"}`; } function runtimeWalletMatches(runtime) { return !runtime?.processAlive || !runtime?.walletId || runtime.walletId === selectedStrategyWalletId(); } function strategyKeyForRuntime(runtime) { if (runtime?.strategyType === "BTC_FIVE_MINUTE_OCO") return "btc_five_minute_oco"; if (runtime?.strategyType === "PP_VALUE") return "pp_value"; if (runtime?.strategyType === "PP_SCORE_VALUE") return "pp_score_value"; if (runtime?.strategyType === "POLY_PRICE") return "poly_price"; if (runtime?.strategyType === "POLY_SETTLEMENT_PAIR") return "poly_settlement_pair"; return "dynamic"; } function strategyLabelForKey(key) { if (key === "btc_five_minute_oco") return "BTC 5分钟双边"; if (key === "poly_settlement_pair") return "临近结算对冲"; if (key === "pp_score_value") return "PP价值分"; if (key === "pp_value") return "PP/h价值"; if (key === "poly_price") return "Poly定价"; return "强弱盘口"; } function selectedAccountStrategyKey() { const walletId = selectedAccountWalletId(); const running = Object.entries(state.strategyRuntimes || {}).find(([, runtime]) => ( runtime?.processAlive && runtime?.walletId === walletId )); if (running) return running[0]; return selectedStrategyType(); } function persistStrategyControls() { localStorage.setItem("btcOrderBudget", $("btcOrderBudget").value); localStorage.setItem("btcFutureMarkets", $("btcFutureMarkets").value); localStorage.setItem(STRATEGY_TYPE_KEY, selectedStrategyType()); localStorage.setItem(STRATEGY_WALLET_KEY, selectedStrategyWalletId()); localStorage.setItem(STRATEGY_MIN_PP_RATE_KEY, selectedStrategyMinPpRate()); } function syncStrategyMinPpDefaultForType(previousType = null) { const input = $("strategyMinPpRateInput"); if (!input) return; const nextDefault = defaultStrategyMinPpRate(); const previousDefault = previousType ? defaultStrategyMinPpRate(previousType) : ""; const current = String(input.value || "").trim(); if (selectedStrategyType() === "poly_price" && current === "16") { input.value = nextDefault; syncStrategyMinPpLabels(); return; } if (!current || (previousDefault && current === previousDefault)) { input.value = nextDefault; } syncStrategyMinPpLabels(); } function renderStrategyWallets(wallets) { state.strategyWallets = wallets || []; const options = state.strategyWallets.map((wallet) => { const address = wallet.accountAddress || wallet.address || ""; const suffix = address ? ` / ${shortAddress(address)}` : ""; const disabled = wallet.ready ? "" : " disabled"; return ``; }).join(""); const fillSelect = (id, storageKey) => { const select = $(id); if (!select) return; const followsAccount = id === "riskWalletSelect" || id === "ppEstimateWalletSelect"; const saved = localStorage.getItem(storageKey) || (followsAccount ? selectedAccountWalletId() : "default"); select.innerHTML = options; if (state.strategyWallets.some((wallet) => wallet.id === saved && wallet.ready)) { select.value = saved; } else if (state.strategyWallets.some((wallet) => wallet.ready)) { select.value = state.strategyWallets.find((wallet) => wallet.ready).id; } }; fillSelect("activeWalletSelect", ACCOUNT_WALLET_KEY); fillSelect("accountWalletSelect", ACCOUNT_WALLET_KEY); fillSelect("strategyWalletSelect", STRATEGY_WALLET_KEY); fillSelect("riskWalletSelect", RISK_WALLET_KEY); fillSelect("ppEstimateWalletSelect", PP_ESTIMATE_WALLET_KEY); syncWalletBoundSelectors(selectedAccountWalletId(), { persist: false }); const riskWallet = state.strategyWallets.find((wallet) => wallet.id === selectedRiskWalletId()); if (riskWallet) { text("riskWallet", `${riskWallet.label || riskWallet.id} / ${shortAddress(riskWallet.accountAddress || riskWallet.address) || "--"}`); } } function syncWalletBoundSelectors(walletId, { persist = true } = {}) { const ready = state.strategyWallets.some((wallet) => wallet.id === walletId && wallet.ready); const fallback = state.strategyWallets.find((wallet) => wallet.ready)?.id || walletId || "default"; const value = ready ? walletId : fallback; for (const id of ["riskWalletSelect", "ppEstimateWalletSelect"]) { const select = $(id); if (select && [...select.options].some((option) => option.value === value && !option.disabled)) { select.value = value; } } if (persist) { localStorage.setItem(RISK_WALLET_KEY, value); localStorage.setItem(PP_ESTIMATE_WALLET_KEY, value); } const riskWallet = state.strategyWallets.find((wallet) => wallet.id === selectedRiskWalletId()); if (riskWallet) { text("riskWallet", `${riskWallet.label || riskWallet.id} / ${shortAddress(riskWallet.accountAddress || riskWallet.address) || "--"}`); } } 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"); if (name === "pricing_deviation") { loadPricingDeviation(false).catch((error) => log("定价偏差加载失败", error.message, "error")); } if (name === "settlement_pair_dry") { loadSettlementPairDry().catch((error) => log("对冲 DRY 加载失败", error.message, "error")); } } 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); const riskWallet = state.strategyWallets.find((wallet) => wallet.id === selectedRiskWalletId()); text("riskWallet", riskWallet ? `${riskWallet.label || riskWallet.id} / ${shortAddress(riskWallet.accountAddress || riskWallet.address) || "--"}` : 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?walletId=${encodeURIComponent(selectedAccountWalletId())}`); 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 (!container) return; 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(); const searchValue = $("search").value.trim(); if (searchValue) params.set("search", searchValue); 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") && (Boolean(searchValue) || 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; } if (scan.dynamic || scan.pp_value || scan.pp_score_value || scan.poly_price || scan.poly_settlement_pair) { const strategyKey = selectedAccountStrategyKey(); const selectedScan = scan[strategyKey] || {}; const total = selectedScan.totalMarketCount ?? "--"; const eligible = selectedScan.eligibleMarketCount ?? 0; const scannedAt = selectedScan.scannedAt ? new Date(selectedScan.scannedAt).toLocaleString("zh-CN", { hour12: false }) : "--"; text("metricMarkets", `${eligible} / ${total}`); text( "marketCount", `${strategyLabelForKey(strategyKey)}:总计 ${total} 个市场,符合条件 ${eligible} 个 · ${scannedAt}`, ); return; } if (scan.dynamic || scan.pp_value || scan.pp_score_value || scan.poly_price || scan.poly_settlement_pair) { const dynamicScan = scan.dynamic || {}; const ppScan = scan.pp_value || {}; const polyScan = scan.poly_price || {}; const dynamicTotal = dynamicScan.totalMarketCount ?? "--"; const ppTotal = ppScan.totalMarketCount ?? "--"; const polyTotal = polyScan.totalMarketCount ?? "--"; const dynamicEligible = dynamicScan.eligibleMarketCount ?? 0; const ppEligible = ppScan.eligibleMarketCount ?? 0; const polyEligible = polyScan.eligibleMarketCount ?? 0; const dynamicTime = dynamicScan.scannedAt ? new Date(dynamicScan.scannedAt).toLocaleString("zh-CN", { hour12: false }) : "--"; const ppTime = ppScan.scannedAt ? new Date(ppScan.scannedAt).toLocaleString("zh-CN", { hour12: false }) : "--"; const polyTime = polyScan.scannedAt ? new Date(polyScan.scannedAt).toLocaleString("zh-CN", { hour12: false }) : "--"; text("metricMarkets", `强弱 ${dynamicEligible}/${dynamicTotal} · PP ${ppEligible}/${ppTotal} · Poly ${polyEligible}/${polyTotal}`); text( "marketCount", `强弱盘口:${dynamicEligible}/${dynamicTotal} · ${dynamicTime};PP/h价值:${ppEligible}/${ppTotal} · ${ppTime};Poly定价:${polyEligible}/${polyTotal} · ${polyTime}`, ); 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/scans"); 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 strategyLabels = { DYNAMIC: "强弱盘口策略", PP_VALUE: "PP/h 价值策略", PP_SCORE_VALUE: "PP 价值分策略", POLY_PRICE: "Poly 定价套利策略", POLY_SETTLEMENT_PAIR: "临近结算对冲策略", }; const modeLabels = { LIVE: "实盘", DRY_RUN: "模拟", PUBLIC_SCAN: "公开扫描", }; const alive = Boolean(runtime?.processAlive); const canClearStoppedRuntime = !alive && runtime?.status === "EXITED"; const walletMatches = runtimeWalletMatches(runtime); const slotOccupiedByOtherWallet = alive && !walletMatches; const selectedWallet = selectedStrategyWallet(); const walletReady = selectedStrategyWalletReady(); const statusText = statusLabels[runtime?.status] || runtime?.status || "已停止"; text("strategyRuntimeStatus", slotOccupiedByOtherWallet ? `${statusText} / 非当前选择钱包` : statusText); text("strategyRuntimeMode", modeLabels[runtime?.mode] || runtime?.mode || "--"); text("strategyRuntimeType", strategyLabels[runtime?.strategyType] || strategyLabelForKey(strategyKeyForRuntime(runtime)) || runtime?.strategyType || "--"); const runningWallet = runtime?.walletLabel || shortAddress(runtime?.walletAddress) || runtime?.walletId || "--"; text("strategyWallet", slotOccupiedByOtherWallet ? `选择 ${walletDisplay(selectedWallet)} / 运行 ${runningWallet}` : (alive ? runningWallet : walletDisplay(selectedWallet))); text("strategyRuntimePid", runtime?.pid || "--"); text("strategyManageAll", runtime?.manageAllOpenOrders ? "开启" : "关闭"); syncStrategyMinPpLabels(runtime?.strategyType || selectedStrategyType()); const runtimeThreshold = runtime?.strategyType === "PP_SCORE_VALUE" ? runtime?.minBuyValueScore : (runtime?.maxBuyCost || runtime?.minScreenPpPerHour || runtime?.minPpRate); text("strategyMinPpRate", runtime?.strategyType === "BTC_FIVE_MINUTE_OCO" ? runtime.buyCostPerSide || "2" : runtimeThreshold || "--"); if (selectedStrategyType() === "btc_five_minute_oco") { text("btcNextMarket", runtimeTime(runtime?.nextMarketAt)); text("btcQuantity", runtime?.quantityPerSide || "--"); text("btcPhase", runtime?.ocoPhase || "--"); text("btcPreparation", ({READY: "已就绪", PREPARING: "准备中", BLOCKED: "未就绪"})[runtime?.preloadState] || "--"); text("btcWalletFeed", runtime?.mode === "DRY_RUN" ? "模拟模式" : runtime?.walletFeedConnected === true ? "已连接" : runtime?.walletFeedConnected === false ? `未连接,暂停新挂单${runtime?.walletFeedError ? ` (${runtime.walletFeedError})` : ""}` : "--"); text("btcPreloaded", (runtime?.preloadedMarkets || []).map(item => `#${item.marketId}`).join(" / ") || "--"); const windows = runtime?.ocoWindows || (runtime?.ocoWindow ? [runtime.ocoWindow] : []); const latestWindow = windows[windows.length - 1]; text("btcRunningCount", runtime?.futureMarkets ?? "--"); text("btcPresignedAt", runtimeTime(latestWindow?.preSignedAt || runtime?.preSignedAt)); const lead = latestWindow?.orders?.[0]?.entryLeadSeconds; text("btcOpeningDelay", lead == null ? "--" : `${lead} 秒`); const summary = runtime?.ocoSettlementSummary; const pnlNumber = value => Number.isFinite(Number(value)) ? Number(value).toFixed(4) : "--"; text("btcSettledCount", summary ? `${summary.settled} / ${summary.pending}` : "--"); text("btcGrossPnl", summary ? pnlNumber(summary.grossPnl) : "--"); const cancelTime = order => order.cancelRoundTripMs == null ? "--" : `${order.cancelRoundTripMs} ms`; $("btcOrders").innerHTML = windows.length ? ` ${windows.flatMap(window => window.orders.map(order => ` `)).join("")}
市场方向价格 / 份额已成交份额状态撤单请求耗时开盘时间本期结束
${escapeHtml(window.title)} #${escapeHtml(window.marketId)} ${escapeHtml(order.outcome)} ${escapeHtml(order.price)} / ${escapeHtml(order.quantity)} ${escapeHtml(order.filled)} ${escapeHtml(order.status)} ${escapeHtml(cancelTime(order))} ${runtimeTime(new Date(window.start * 1000).toISOString())} ${runtimeTime(new Date(window.end * 1000).toISOString())}
${windows.filter(window => window.warning).map(window => `

#${escapeHtml(window.marketId)} ${escapeHtml(window.warning)}

`).join("")}` : '
暂无订单
'; const history = runtime?.ocoHistory || []; text("btcHistoryCount", `最近 ${history.length} / 共 ${runtime?.ocoHistoryCount ?? history.length} 期`); $("btcHistory").innerHTML = history.length ? ` ${history.map(window => { const settlement = window.settlement || {}; const filled = new Map((settlement.orders || []).map(order => [order.hash, order.filled])); const status = ({SETTLED: "已核算(非赎回记录)", WAITING_RESULT: "待平台判定", WAITING_FILLS: "待成交核对", RETRY: "查询重试中"})[settlement.status] || "待核算"; return ``; }).join("")}
市场 / 结束时间成交 / 订单状态撤单请求耗时平台判定核算状态成交成本 USDT毛兑付额 USDT毛盈亏 USDT净盈亏(不含 Gas)USDT
${escapeHtml(window.title)} #${escapeHtml(window.marketId)}
${runtimeTime(new Date(window.end * 1000).toISOString())}
${window.orders.map(order => `${escapeHtml(order.outcome)}: ${escapeHtml(filled.get(order.hash) ?? order.filled)} / ${escapeHtml(order.status)}`).join("
")}
${window.orders.map(order => `${escapeHtml(order.outcome)}: ${escapeHtml(cancelTime(order))}`).join("
")}
${escapeHtml(settlement.winner || "--")} ${status} ${settlement.cost == null ? "--" : pnlNumber(settlement.cost)} ${settlement.payout == null ? "--" : pnlNumber(settlement.payout)} ${settlement.grossPnl == null ? "--" : pnlNumber(settlement.grossPnl)} ${settlement.netPnl == null ? (settlement.status === "SETTLED" ? "费用待核对" : "--") : pnlNumber(settlement.netPnl)}
` : '
暂无历史期次
'; } text("strategyStartedAt", runtimeTime(runtime?.startedAt)); text("strategyLastCycleAt", runtimeTime(runtime?.lastCycleAt)); text( "strategyInterval", runtime?.intervalSeconds != null ? `${runtime.intervalSeconds} 秒` : "--", ); $("strategyRuntimeError").textContent = runtime?.lastError || (slotOccupiedByOtherWallet ? "该策略类型正在另一个策略钱包运行;停止和重筛仍按策略类型执行。" : (walletReady ? "无运行错误" : "当前策略钱包未配置私钥")); $("startDryStrategy").disabled = alive || !walletReady; $("startLiveStrategy").disabled = alive || !walletReady; $("forceStrategyRescan").disabled = !alive; $("stopStrategy").disabled = !alive && !canClearStoppedRuntime; $("manageAllOpenOrders").disabled = alive; if ($("strategyType")) $("strategyType").disabled = false; if ($("strategyMinPpRateInput")) $("strategyMinPpRateInput").disabled = alive; $("btcOrderBudget").disabled = alive; $("btcFutureMarkets").disabled = alive; } function renderStrategySlots(runtimes) { state.strategyRuntimes = runtimes || {}; const out = $("strategySlotsOut"); if (!out) return; const rows = [ ["dynamic", state.strategyRuntimes.dynamic], ["pp_value", state.strategyRuntimes.pp_value], ["pp_score_value", state.strategyRuntimes.pp_score_value], ["poly_price", state.strategyRuntimes.poly_price], ["poly_settlement_pair", state.strategyRuntimes.poly_settlement_pair], ["btc_five_minute_oco", state.strategyRuntimes.btc_five_minute_oco], ]; out.innerHTML = ` ${rows.map(([key, runtime]) => { const name = key === "btc_five_minute_oco" ? "BTC 5分钟双边策略" : key === "pp_value" ? "PP/h 价值策略" : (key === "pp_score_value" ? "PP 价值分策略" : (key === "poly_price" ? "Poly 定价套利策略" : (key === "poly_settlement_pair" ? "临近结算对冲策略" : "强弱盘口策略"))); const alive = runtime?.processAlive ? "运行中" : "已停止"; const walletLabel = runtime?.walletLabel || shortAddress(runtime?.walletAddress) || runtime?.walletId || "--"; return ` `; }).join("")}
策略状态模式钱包PID最近一轮
${name} ${escapeHtml(runtime?.status || alive)} ${escapeHtml(runtime?.mode || "--")} ${escapeHtml(walletLabel)} ${escapeHtml(runtime?.pid || "--")} ${runtimeTime(runtime?.lastCycleAt)}
`; const headerRow = out.querySelector("thead tr"); if (headerRow) { headerRow.innerHTML = "市场结果挂单当前每小时本周估算"; } } async function loadStrategyRuntime() { const runtime = await api(`/api/strategy/runtime?strategyType=${encodeURIComponent(selectedStrategyType())}`); renderStrategyRuntime(runtime); return runtime; } async function loadAllStrategyRuntimes() { const runtimes = await api("/api/strategy/runtime?strategyType=all"); renderStrategySlots(runtimes); return runtimes; } function runningStrategiesForWallet(walletId, runtimes) { const targetWalletId = String(walletId || "default"); return Object.entries(runtimes || {}).filter(([, runtime]) => ( runtime?.processAlive && String(runtime?.walletId || "default") === targetWalletId )); } async function loadStrategyWallets() { const payload = await api("/api/strategy/wallets"); renderStrategyWallets(payload.wallets || []); if (state.strategyRuntime) renderStrategyRuntime(state.strategyRuntime); return payload; } function renderWalletPp(payload) { const out = $("walletPpOut"); if (!out) return; const selectedWalletId = selectedPPEstimateWalletId(); const allWallets = payload?.wallets || []; state.walletPp = allWallets; const wallets = allWallets.filter((wallet) => wallet.id === selectedWalletId); if (!wallets.length) { out.innerHTML = `
当前钱包暂无 PP/h 数据
`; return; } out.innerHTML = ` ${wallets.map((wallet) => { const top = wallet.orders || []; return ` `; }).join("")}
钱包当前 PP/h挂单最高贡献
${escapeHtml(wallet.label || wallet.id)} ${escapeHtml(shortAddress(wallet.accountAddress || wallet.address) || "--")} 钱包 ID: ${escapeHtml(wallet.id)} ${wallet.error ? `${escapeHtml(wallet.error)}` : ""} ${escapeHtml(wallet.currentHourlyPp || "0.0000")} PP/h ${escapeHtml(wallet.openOrderCount ?? 0)} 单 / ${escapeHtml(wallet.estimatedTokenCount ?? 0)} 个盘口 ${top.slice(0, 12).map((row) => `
${escapeHtml(row.currentHourlyPp)} PP/h ${escapeHtml(row.title || row.marketId)} / ${escapeHtml(row.outcome || "--")} / #${escapeHtml(row.marketId)}
`).join("") || "--"}
`; } async function loadWalletPp() { const payload = await api("/api/strategy/wallet-pp"); renderWalletPp(payload); return payload; } async function startStrategy(mode) { const manageAllOpenOrders = selectedStrategyType() !== "btc_five_minute_oco" && manageAllOpenOrdersEnabled(); persistStrategyControls(); const strategyType = selectedStrategyType(); const walletId = selectedStrategyWalletId(); const minPpRate = selectedStrategyMinPpRate(); if (mode === "LIVE" && !window.confirm( manageAllOpenOrders ? "确认启动实盘策略并全面接管挂单?策略将可以撤销账号内未登记为策略单的挂单。" : "确认启动实盘策略?策略将提交和取消真实订单。", )) { return; } persistManageAllOpenOrders(); const runtime = await api("/api/strategy/start", { method: "POST", body: JSON.stringify({ mode, manageAllOpenOrders, strategyType, walletId, minPpRate, buyCostPerSide: $("btcOrderBudget").value, futureMarkets: $("btcFutureMarkets").value }), }); renderStrategyRuntime(runtime); log(mode === "LIVE" ? "实盘策略启动中" : "模拟策略启动中", runtime); window.setTimeout(() => { loadStrategyRuntime().catch(() => {}); loadAllStrategyRuntimes().catch(() => {}); loadStrategyScan().catch(() => {}); }, 1000); } async function stopStrategy() { const strategyType = selectedStrategyType(); const runtime = await api("/api/strategy/stop", { method: "POST", body: JSON.stringify({ strategyType }), }); renderStrategyRuntime(runtime); log("策略停止请求已发送", runtime); window.setTimeout(() => { loadStrategyRuntime().catch(() => {}); loadAllStrategyRuntimes().catch(() => {}); loadStrategyScan().catch(() => {}); }, 1200); } async function forceStrategyRescan() { const strategyType = selectedStrategyType(); const runtime = await api("/api/strategy/rescan", { method: "POST", body: JSON.stringify({ strategyType }), }); renderStrategyRuntime(runtime); log("已请求策略立即重筛市场,下一轮轮询会执行", runtime); window.setTimeout(() => { loadStrategyRuntime().catch(() => {}); loadAllStrategyRuntimes().catch(() => {}); loadStrategyScan().catch(() => {}); }, 5000); } function orderSummaryText(row) { const orders = Array.isArray(row?.orders) ? row.orders : []; if (!orders.length) return "--"; return orders.map((order) => { const side = order.side === "BUY" ? "买入" : (order.side === "SELL" ? "卖出" : (order.side || "--")); const price = order.price ?? "--"; const quantity = order.quantity ?? "--"; const metrics = order.side === "BUY" && (order.riskScore || order.valueScore) ? `风险分 ${escapeHtml(order.riskScore || "--")} · 价值分 ${escapeHtml(order.valueScore || "--")}` : ""; return `${side} ${escapeHtml(price)} x ${escapeHtml(quantity)}${metrics}`; }).join("
"); } function renderPPEstimate(payload) { text("ppEstimateWallet", payload?.walletLabel || shortAddress(payload?.accountAddress || payload?.walletAddress) || selectedPPEstimateWalletId()); text("ppCurrentHourly", `${payload?.currentHourlyPp ?? "0.0000"} PP/h`); text("ppWeeklyTotal", `${payload?.weeklyEstimatedPp ?? "0.0000"} PP`); text("ppCoveredHours", `${payload?.coveredHours ?? "0.0000"} h`); text("ppLatestBookAt", runtimeTime(payload?.latestOrderbookAt)); const out = $("ppEstimateOut"); if (!out) return; const rows = payload?.top || []; if (!rows.length) { out.innerHTML = `
${escapeHtml(payload?.message || "暂无可计算的 PP 贡献")}
`; return; } out.innerHTML = ` ${rows.map((row) => { const parentMarket = row.parentMarket || row.question || ""; return ` `; }).join("")}
市场结果当前每小时本周估算
${escapeHtml(row.title || row.marketId)} ${parentMarket ? `所属市场:${escapeHtml(parentMarket)}` : ""} #${escapeHtml(row.marketId)} / ${escapeHtml(row.outcome || "--")} ${escapeHtml(row.outcome || "--")} ${orderSummaryText(row)}${row.orderCount ? `${escapeHtml(row.orderCount)} 单` : ""} ${escapeHtml(row.currentHourlyPp)} PP/h ${escapeHtml(row.weeklyEstimatedPp)} PP
`; } function quoteText(price, size) { if (price == null || price === "") return "--"; const sizeText = size == null || size === "" ? "" : ` x ${size}`; return `${price}${sizeText}`; } function edgeClass(value) { const number = Number(value || 0); if (number > 0) return "okText"; if (number < 0) return "badText"; return "mutedLine"; } function deviationDirectionText(row) { if (row.direction === "BUY_PREDICT_SELL_POLY") return "买 Predict / 卖 Poly"; if (row.direction === "BUY_POLY_SELL_PREDICT") return "买 Poly / 卖 Predict"; return "无正价差"; } function renderPricingDeviation(payload) { text("pricingCandidateCount", payload?.candidateMarketCount ?? "--"); text("pricingRowCount", payload?.rowCount ?? "--"); text("pricingScannedAt", runtimeTime(payload?.scannedAt)); text("pricingSource", payload?.source || "--"); const out = $("pricingDeviationOut"); if (!out) return; const rows = payload?.rows || []; if (!rows.length) { const errorText = (payload?.errors || []).slice(0, 3).map((row) => `${row.marketId}: ${row.error}`).join("\n"); out.innerHTML = `
${escapeHtml(errorText || "没有找到可对比的 Polymarket 映射盘口")}
`; return; } out.innerHTML = ` ${rows.map((row) => ` `).join("")}
Predict 市场 结果 Predict 买一 / 卖一 Polymarket 买一 / 卖一 价差空间 方向
${escapeHtml(row.title || row.marketId)} ${row.question ? `${escapeHtml(row.question)}` : ""} Predict #${escapeHtml(row.marketId)} / Poly #${escapeHtml(row.polymarketMarketId || "--")} ${escapeHtml(row.outcome || "--")} ${escapeHtml(row.polymarketOutcome || "--")} ${row.displayReversed ? `由 ${escapeHtml(row.predictOutcome || "--")} 反向显示` : ""} 买 ${escapeHtml(quoteText(row.predictBid, row.predictBidSize))} 卖 ${escapeHtml(quoteText(row.predictAsk, row.predictAskSize))} 买 ${escapeHtml(quoteText(row.polyBid, row.polyBidSize))} 卖 ${escapeHtml(quoteText(row.polyAsk, row.polyAskSize))} P买/Poly卖 ${escapeHtml(row.buyPredictSellPoly ?? "--")} Poly买/P卖 ${escapeHtml(row.buyPolySellPredict ?? "--")} 绝对偏差 ${escapeHtml(row.absoluteGap ?? "--")} ${escapeHtml(deviationDirectionText(row))} PP ${escapeHtml(row.rewardPpPerHour || "0.0000")}/h
`; } async function loadPricingDeviation(refresh = false) { const params = new URLSearchParams(); params.set("maxMarkets", String($("pricingMaxMarkets")?.value || "500")); params.set("limit", "100"); if (refresh) params.set("refresh", "1"); const payload = await api(`/api/pricing-deviation?${params.toString()}`); renderPricingDeviation(payload); return payload; } function settlementDryRows(rows, kind) { if (!rows?.length) return `
暂无${kind}
`; return ` ${rows.map((row) => { const ratio = Number(row.expectedProfitRatio || 0) * 100; return ` `; }).join("")}
市场结果价格 / 数量状态预期结算预期收益
${escapeHtml(row.title || `Market ${row.marketId}`)}
#${escapeHtml(row.marketId || "--")} · ${escapeHtml(row.action || "--")}
${escapeHtml(row.outcome || "--")} ${escapeHtml(row.side || "--")} ${escapeHtml(row.price || "--")} x ${escapeHtml(row.quantity || "--")} ${escapeHtml(row.filledByRule ? "已成交" : (row.status || "--"))} ${runtimeTime(row.settlementAt)} ${escapeHtml(row.expectedProfitUsd || "--")} USDT
${Number.isFinite(ratio) ? ratio.toFixed(2) : "--"}%
`; } function renderSettlementVenue(name, venue) { return `

${escapeHtml(name)}

可用 ${escapeHtml(venue?.available || venue?.cash || "200.0000")} USDT 挂单占用 ${escapeHtml(venue?.reserved || "0.0000")} USDT 权益 ${escapeHtml(venue?.equity || "200.0000")} USDT

挂单明细

${settlementDryRows(venue?.orders || [], "挂单")}

成交明细

${settlementDryRows(venue?.fills || [], "成交")}
`; } function renderSettlementPairDry(payload) { const status = $("settlementPairDryStatus"); const out = $("settlementPairDryOut"); if (!status || !out) return; if (!payload?.running) { status.style.display = ""; status.textContent = payload?.message || "暂无 DRY 策略运行"; out.innerHTML = ""; return; } status.style.display = "none"; const candidates = payload.candidates || []; out.innerHTML = `
扫描 ID
${escapeHtml(payload.scanId || "--")}
最近扫描
${runtimeTime(payload.scannedAt)}
候选数量
${candidates.length} / 5
模式
${escapeHtml(payload.mode || "DRY_RUN")}
${renderSettlementVenue("Predict 模拟账户", payload.predict || {})} ${renderSettlementVenue("Polymarket 模拟账户", payload.poly || {})}
`; } async function loadSettlementPairDry() { const payload = await api("/api/strategy/settlement-pair-dry"); renderSettlementPairDry(payload); return payload; } async function clearSettlementPairDry() { const result = await api("/api/strategy/settlement-pair-dry/clear", { method: "POST" }); log("临近结算对冲 DRY 历史已清空", result); await loadSettlementPairDry(); } function renderPPEstimateRuntime(runtime) { const labels = { STARTING: "启动中", BACKFILLING: "回填中", RUNNING: "运行中", STOPPING: "停止中", STOPPED: "已停止", EXITED: "进程已退出", }; const alive = Boolean(runtime?.processAlive); const selectedWallet = state.strategyWallets.find((wallet) => wallet.id === selectedPPEstimateWalletId()); const selectedLabel = selectedWallet?.label || selectedPPEstimateWalletId(); const runtimeWalletIds = Array.isArray(runtime?.walletIds) ? runtime.walletIds.map(String) : (runtime?.walletId && runtime.walletId !== "all" ? [String(runtime.walletId)] : []); const walletMatches = !alive || runtime?.walletId === "all" || runtimeWalletIds.includes(selectedPPEstimateWalletId()); const walletLabel = runtimeWalletIds.length > 1 ? "全部钱包" : (runtime?.walletLabel || runtimeWalletIds[0] || runtime?.walletId || ""); const statusText = labels[runtime?.status] || runtime?.status || "已停止"; text("ppRuntimeStatus", alive && !walletMatches ? `${statusText} / ${walletLabel};当前 ${selectedLabel} 未运行` : (walletLabel ? `${statusText} / ${walletLabel}` : statusText)); const toggle = $("togglePPEstimate"); if (toggle) { toggle.textContent = alive ? (walletMatches ? "停止计算" : "切换计算到当前钱包") : "启动计算"; toggle.classList.toggle("danger", alive && walletMatches); toggle.classList.toggle("primary", !alive || !walletMatches); } } async function loadPPEstimate() { const payload = await api(`/api/strategy/pp-estimate?walletId=${encodeURIComponent(selectedPPEstimateWalletId())}`); renderPPEstimate(payload); return payload; } async function loadPPEstimateRuntime() { const runtime = await api("/api/strategy/pp-estimate/runtime"); renderPPEstimateRuntime(runtime); return runtime; } async function startPPEstimate() { const runtime = await api("/api/strategy/pp-estimate/start", { method: "POST", body: JSON.stringify({ walletIds: "all" }), }); renderPPEstimateRuntime(runtime); log("PP 计算进程启动中", runtime); window.setTimeout(() => { loadPPEstimateRuntime().catch(() => {}); loadPPEstimate().catch(() => {}); }, 1500); } async function stopPPEstimate() { const runtime = await api("/api/strategy/pp-estimate/stop", { method: "POST" }); renderPPEstimateRuntime(runtime); log("PP 计算进程停止请求已发送", runtime); window.setTimeout(() => { loadPPEstimateRuntime().catch(() => {}); }, 1000); return runtime; } async function togglePPEstimate() { const runtime = await loadPPEstimateRuntime(); if (runtime?.processAlive) { const runtimeWalletIds = Array.isArray(runtime?.walletIds) ? runtime.walletIds.map(String) : (runtime?.walletId && runtime.walletId !== "all" ? [String(runtime.walletId)] : []); const coversSelected = runtime?.walletId === "all" || runtimeWalletIds.includes(selectedPPEstimateWalletId()); if (!coversSelected) { await stopPPEstimate(); window.setTimeout(() => { startPPEstimate().catch((error) => log("PP 计算切换失败", error.message, "error")); }, 1200); return runtime; } return stopPPEstimate(); } return startPPEstimate(); } 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 (!container) return; 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&walletId=${encodeURIComponent(selectedAccountWalletId())}`); state.orders = orderRows(result); renderOrders(); log(`已加载 ${state.orders.length} 个开放订单`); return result; } async function loadPositions() { const result = await api(`/api/positions?first=100&walletId=${encodeURIComponent(selectedAccountWalletId())}`); state.positions = positionRows(result); renderPositions(); log(`已加载 ${state.positions.length} 个持仓`); return result; } async function loadAccount() { const walletId = encodeURIComponent(selectedAccountWalletId()); const [accountResult, balanceResult] = await Promise.allSettled([ api(`/api/account?walletId=${walletId}`), api(`/api/balance?walletId=${walletId}`), ]); 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 loadAccountSummary() { const walletId = encodeURIComponent(selectedAccountWalletId()); const summary = await api(`/api/account/summary?walletId=${walletId}`); const amount = summary?.balanceUsdt ?? summary?.balance?.usdt ?? summary?.balance?.data?.usdt ?? "--"; text("metricBalance", amount); text("overviewBalance", `${amount} USDT`); text("metricOrders", summary?.openOrderCount ?? "--"); text("metricPositions", summary?.positionCount ?? "--"); text("overviewAddress", summary?.account?.data?.address || summary?.account?.address || summary?.status?.address || state.status?.address); if (summary?.status) renderStatus({ ...summary.status, ...summary }); if (summary?.errors && Object.keys(summary.errors).length) { log("账户汇总部分加载失败", summary.errors, "error"); } return summary; } async function refreshAccountData() { const results = await Promise.allSettled([ loadAccount(), loadOrders(), loadPositions(), ]); const labels = ["账户数据", "订单", "持仓"]; results.forEach((result, index) => { if (result.status === "rejected") { log(`${labels[index]}加载失败`, result.reason.message, "error"); } }); return results; } async function refreshActiveAccountTables() { const tasks = []; if (state.activeView === "orders") tasks.push(loadOrders()); if (state.activeView === "positions") tasks.push(loadPositions()); await Promise.allSettled(tasks); } async function refreshOverview() { const status = await loadStatus(); await loadAllStrategyRuntimes().catch((error) => log("策略运行状态加载失败", error.message, "error")); const tasks = [ loadStrategyScan().catch((error) => { renderStrategyScan(null); log("策略筛选统计加载失败", error.message, "error"); }), ]; if (selectedAccountWalletReady() || status.privateKeyLoaded) { tasks.push(loadAccountSummary()); tasks.push(refreshActiveAccountTables()); } else { text("metricBalance", "--"); text("metricOrders", "--"); text("metricPositions", "--"); } await Promise.allSettled(tasks); const now = formatTime(); text("metricSync", now); text("overviewSync", now); log("总览数据同步完成"); } async function refreshWalletOverview() { const [statusResult] = await Promise.allSettled([ loadStatus(), loadAllStrategyRuntimes(), ]); await loadStrategyScan().catch((error) => { renderStrategyScan(null); log("策略筛选统计加载失败", error.message, "error"); }); const status = statusResult.status === "fulfilled" ? statusResult.value : state.status; if (selectedAccountWalletReady() || status?.privateKeyLoaded) { await Promise.allSettled([ loadAccountSummary(), refreshActiveAccountTables(), ]); } else { text("metricBalance", "--"); text("metricOrders", "--"); text("metricPositions", "--"); } const now = formatTime(); text("metricSync", now); text("overviewSync", now); } function setAccountMetricsLoading() { text("metricBalance", "加载中"); text("overviewBalance", "加载中"); text("metricOrders", "..."); text("metricPositions", "..."); text("metricSync", "同步中"); text("overviewSync", "同步中"); } 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 = { walletId: selectedAccountWalletId(), 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 walletId = selectedRiskWalletId(); const path = run ? `/api/approvals/${state.selectedMarket}/run` : `/api/approvals/${state.selectedMarket}?walletId=${encodeURIComponent(walletId)}`; const result = await api(path, run ? { method: "POST", body: JSON.stringify({ walletId }), } : {}); $("riskOutput").textContent = JSON.stringify(result, null, 2); log(run ? "授权执行完成" : "授权检查完成", result); return result; } async function strategyApprovalAction(run) { const walletId = selectedRiskWalletId(); if (run) { const confirmed = window.confirm( "确认对该钱包执行全部交易授权?这会发送链上交易并可能消耗 BNB Gas。", ); if (!confirmed) return; } const result = await api( run ? "/api/strategy/approvals/run" : `/api/strategy/approvals?walletId=${encodeURIComponent(walletId)}&scope=all`, run ? { method: "POST", body: JSON.stringify({ confirm: "AUTHORIZE_STRATEGY_MARKETS", walletId, scope: "all", }), } : {}, ); $("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")); }); const manageAllOpenOrdersToggle = $("manageAllOpenOrders"); if (manageAllOpenOrdersToggle) { manageAllOpenOrdersToggle.checked = localStorage.getItem(MANAGE_ALL_OPEN_ORDERS_KEY) === "1"; manageAllOpenOrdersToggle.addEventListener("change", persistManageAllOpenOrders); } const strategyTypeSelect = $("strategyType"); if (strategyTypeSelect) { strategyTypeSelect.value = localStorage.getItem(STRATEGY_TYPE_KEY) || "dynamic"; let previousStrategyType = strategyTypeSelect.value; strategyTypeSelect.addEventListener("change", () => { syncStrategyMinPpDefaultForType(previousStrategyType); previousStrategyType = selectedStrategyType(); persistStrategyControls(); loadStrategyRuntime().catch((error) => log("策略状态加载失败", error.message, "error")); loadStrategyScan().catch((error) => log("策略筛选加载失败", error.message, "error")); }); } const strategyMinPpRateInput = $("strategyMinPpRateInput"); $("btcOrderBudget").value = localStorage.getItem("btcOrderBudget") || "2"; $("btcOrderBudget").addEventListener("change", persistStrategyControls); $("btcFutureMarkets").value = localStorage.getItem("btcFutureMarkets") || "3"; $("btcFutureMarkets").addEventListener("change", persistStrategyControls); if (strategyMinPpRateInput) { strategyMinPpRateInput.value = localStorage.getItem(STRATEGY_MIN_PP_RATE_KEY) || defaultStrategyMinPpRate(); syncStrategyMinPpDefaultForType(); strategyMinPpRateInput.addEventListener("change", persistStrategyControls); } const strategyWalletSelect = $("strategyWalletSelect"); if (strategyWalletSelect) { strategyWalletSelect.addEventListener("change", () => { persistStrategyControls(); if (state.strategyRuntime) renderStrategyRuntime(state.strategyRuntime); }); } const accountWalletSelect = $("accountWalletSelect"); if (accountWalletSelect) { accountWalletSelect.addEventListener("change", () => { localStorage.setItem(ACCOUNT_WALLET_KEY, selectedAccountWalletId()); syncWalletBoundSelectors(selectedAccountWalletId()); setAccountMetricsLoading(); refreshWalletOverview().catch((error) => log("账户钱包切换刷新失败", error.message, "error")); if (state.activeView === "pp_estimate") { loadPPEstimate().catch((error) => log("PP 估算刷新失败", error.message, "error")); loadPPEstimateRuntime().catch((error) => log("PP 计算状态刷新失败", error.message, "error")); } }); } const ppEstimateWalletSelect = $("ppEstimateWalletSelect"); if (ppEstimateWalletSelect) { ppEstimateWalletSelect.addEventListener("change", () => { localStorage.setItem(PP_ESTIMATE_WALLET_KEY, selectedPPEstimateWalletId()); loadPPEstimate().catch((error) => log("PP 估算钱包切换失败", error.message, "error")); loadPPEstimateRuntime().catch((error) => log("PP 计算状态刷新失败", error.message, "error")); }); } const riskWalletSelect = $("riskWalletSelect"); if (riskWalletSelect) { riskWalletSelect.addEventListener("change", () => { localStorage.setItem(RISK_WALLET_KEY, selectedRiskWalletId()); const wallet = state.strategyWallets.find((item) => item.id === selectedRiskWalletId()); if (wallet) { text("riskWallet", `${wallet.label || wallet.id} / ${shortAddress(wallet.accountAddress || wallet.address) || "--"}`); } }); } wire("refreshAll", refreshOverview); wire("settingsRefresh", loadStatus); wire("refreshAccount", refreshAccountData); wire("refreshAuth", async () => { await api("/api/auth/refresh", { method: "POST", body: JSON.stringify({ walletId: selectedAccountWalletId() }), }); 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("refreshSettlementPairDry", loadSettlementPairDry); wire("clearSettlementPairDry", clearSettlementPairDry); wire("togglePPEstimate", togglePPEstimate); wire("refreshPricingDeviation", () => loadPricingDeviation(true)); 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, walletId: selectedAccountWalletId() }) }); 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, walletId: selectedAccountWalletId() }) }); 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); loadStrategyWallets() .catch((error) => log("策略钱包加载失败", error.message, "error")) .finally(() => refreshOverview().catch((error) => log("初始化失败", error.message, "error"))); loadStrategyRuntime().catch((error) => log("策略状态加载失败", error.message, "error")); loadAllStrategyRuntimes().catch((error) => log("策略运行状态加载失败", error.message, "error")); loadPPEstimateRuntime().catch((error) => log("PP 计算状态加载失败", error.message, "error")); loadPPEstimate().catch((error) => log("PP 估算加载失败", error.message, "error")); loadDisabledMarkets().catch((error) => log("禁用市场加载失败", error.message, "error")); window.setInterval(() => { loadStrategyRuntime().catch(() => {}); loadAllStrategyRuntimes().catch(() => {}); if (state.activeView === "settlement_pair_dry") loadSettlementPairDry().catch(() => {}); }, 3000); window.setInterval(() => { loadPPEstimate().catch(() => {}); }, 60000); window.setInterval(() => { loadPPEstimateRuntime().catch(() => {}); }, 5000);