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 = `
| 市场 | 比赛时间 | 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();
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 => `
| ${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())} |
`)).join("")}
${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 ? `
| 市场 / 结束时间 | 成交 / 订单状态 | 撤单请求耗时 | 平台判定 | 核算状态 | 成交成本 USDT | 毛兑付额 USDT | 毛盈亏 USDT | 净盈亏(不含 Gas)USDT |
${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 `
${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)} |
`;
}).join("")}
` : '暂无历史期次
';
}
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 = `
| 策略 | 状态 | 模式 | 钱包 | PID | 最近一轮 |
${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 `
| ${name} |
${escapeHtml(runtime?.status || alive)} |
${escapeHtml(runtime?.mode || "--")} |
${escapeHtml(walletLabel)} |
${escapeHtml(runtime?.pid || "--")} |
${runtimeTime(runtime?.lastCycleAt)} |
`;
}).join("")}
`;
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 = `
| 钱包 | 当前 PP/h | 挂单 | 最高贡献 |
${wallets.map((wallet) => {
const top = wallet.orders || [];
return `
|
${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("") || "--"}
|
`;
}).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 `
|
${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 |
`;
}).join("")}
`;
}
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 = `
| Predict 市场 |
结果 |
Predict 买一 / 卖一 |
Polymarket 买一 / 卖一 |
价差空间 |
方向 |
${rows.map((row) => `
|
${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
|
`).join("")}
`;
}
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 `
${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) : "--"}% |
`;
}).join("")}
`;
}
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 = `
| 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 (!container) return;
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&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);