window.XiaobaiPageModules.register("pools", [ "limitPool", "brokenView", "downView", "yesterdayView", "performanceView", ]); /* PRESERVATION-SOURCE-BEGIN app.js:1517-1915 */ function getVisibleStocks() { if (!state.dashboard) return []; let rows = [...(state.dashboard.limits || [])]; if (state.filter === "1") rows = rows.filter((row) => number(row.streak) === 1); if (state.filter === "2") rows = rows.filter((row) => number(row.streak) === 2); if (state.filter === "3") rows = rows.filter((row) => number(row.streak) >= 3); if (state.query) { rows = rows.filter((row) => { const haystack = `${row.code} ${row.name} ${row.sector} ${row.reason}`.toLowerCase(); return haystack.includes(state.query); }); } return rows.sort((left, right) => compareRows(left, right)); } function renderLimitTable() { if (!state.dashboard) return; const rows = getVisibleStocks(); const allRows = state.dashboard.limits || []; const body = document.querySelector("#limitTableBody"); body.innerHTML = rows.map((row, index) => ` ${index + 1} ${escapeHtml(row.name)}${escapeHtml(row.code)} ${streakLabel(row.streak)} ${signed(row.change)} ${formatNumber(row.price, 2)} ${escapeHtml(row.sector || "其他")} ${escapeHtml(row.first_time || "")} ${escapeHtml(row.last_time || "")} ${limitOpenState(row)} ${formatNumber(row.turnover_rate, 2)} ${formatNumber(row.amount_billion, 2)} ${formatLimitSealAmount(row.seal_amount_million)} ${escapeHtml(row.reason || "")} `).join(""); bindStockRows(body); setText("resultCount", `${rows.length} 只`); setText("limitPoolSubtitle", `${allRows.length} 只 · 数据日期 ${displayCompactDate(state.dashboard.meta?.trade_date || elements.tradeDate.value)}`); setText("limitAllCount", allRows.length); setText("limitFirstCount", allRows.filter((row) => number(row.streak) === 1).length); setText("limitSecondCount", allRows.filter((row) => number(row.streak) === 2).length); setText("limitThreePlusCount", allRows.filter((row) => number(row.streak) >= 3).length); document.querySelector("#emptyState").hidden = rows.length !== 0; updateSortHeaders(); } function limitOpenState(row) { const openTimes = number(row.open_times); const firstTime = String(row.first_time || ""); if (firstTime.startsWith("09:25") && openTimes === 0) return '一字'; if (openTimes >= 6) return `烂板×${openTimes}`; return String(openTimes); } function formatLimitSealAmount(value) { const amount = number(value); if (!amount) return ""; return Math.round(amount).toLocaleString("zh-CN"); } function renderBrokenTable(rows) { const visibleRows = getVisibleBrokenRows(rows); setText("brokenCount", `${rows.length} 只`); setText("brokenMeta", ` · 触及涨停后未能封住 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`); const body = document.querySelector("#brokenTableBody"); body.innerHTML = visibleRows.map((row, index) => ` ${index + 1} ${escapeHtml(row.name)}${escapeHtml(row.code)} ${signed(row.change)} ${formatNumber(row.limitGap, 2)} ${formatNumber(row.price, 2)} ${escapeHtml(row.sector || "其他")} ${escapeHtml(row.first_time || "")} ${brokenOpenState(row)} ${formatNumber(row.turnover_rate, 2)} ${formatNumber(row.amount_billion, 2)} ${escapeHtml(row.reason || "")} `).join(""); bindStockRows(body); document.querySelector("#brokenEmptyState").hidden = visibleRows.length !== 0; updateBrokenSortHeaders(); } function getVisibleBrokenRows(rows = state.dashboard?.broken || []) { let visibleRows = rows.map((row) => ({ ...row, limitGap: brokenLimitGap(row) })); if (state.brokenQuery) { visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.brokenQuery)); } if (!state.brokenSortKey) return visibleRows; return visibleRows.sort((left, right) => { const result = number(left[state.brokenSortKey]) - number(right[state.brokenSortKey]); return state.brokenSortDirection === "asc" ? result : -result; }); } function brokenLimitRate(row) { const name = String(row.name || "").toUpperCase(); const code = String(row.code || "").replace(/\D/g, ""); if (name.includes("ST")) return 10; if (/^(300|301|688|689)/.test(code)) return 20; if (/^(4|8|92)/.test(code)) return 30; return 10; } function brokenLimitGap(row) { return Math.max(0, brokenLimitRate(row) - number(row.change)); } function brokenOpenState(row) { const openTimes = number(row.open_times); return openTimes >= 6 ? `反复炸 ×${openTimes}` : String(openTimes); } function changeBrokenSort(key) { if (state.brokenSortKey === key) state.brokenSortDirection = state.brokenSortDirection === "asc" ? "desc" : "asc"; else { state.brokenSortKey = key; state.brokenSortDirection = "desc"; } renderBrokenTable(state.dashboard?.broken || []); } function updateBrokenSortHeaders() { document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => { header.classList.remove("sort-asc", "sort-desc", "sorted"); header.setAttribute("aria-sort", "none"); if (header.dataset.brokenSort === state.brokenSortKey) { header.classList.add(state.brokenSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted"); header.setAttribute("aria-sort", state.brokenSortDirection === "asc" ? "ascending" : "descending"); } const arrow = header.querySelector(".arr"); if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.brokenSortDirection === "asc" ? "▲" : "▼") : "↕"; }); } function renderDownTable(rows) { const visibleRows = getVisibleDownRows(rows); setText("downCount", `${rows.length} 只`); setText("downMeta", ` · 观察退潮、高位风险与亏钱效应 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`); renderDownSectorCluster(rows); const body = document.querySelector("#downTableBody"); body.innerHTML = visibleRows.map((row, index) => ` ${index + 1} ${escapeHtml(row.name)}${escapeHtml(row.code)} ${signed(row.change)} ${formatNumber(row.price, 2)} ${escapeHtml(row.sector || "其他")} ${formatNumber(row.turnover_rate, 2)} ${formatNumber(row.amount_billion, 2)} ${number(row.streak) > 0 ? number(row.streak) : ""} ${escapeHtml(row.reason || "")} `).join(""); bindStockRows(body); document.querySelector("#downEmptyState").hidden = visibleRows.length !== 0; updateDownSortHeaders(); } function getVisibleDownRows(rows = state.dashboard?.down_limits || []) { let visibleRows = [...rows]; if (state.downQuery) { visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.downQuery)); } if (!state.downSortKey) return visibleRows; return visibleRows.sort((left, right) => { const result = number(left[state.downSortKey]) - number(right[state.downSortKey]); return state.downSortDirection === "asc" ? result : -result; }); } function renderDownSectorCluster(rows) { const counts = new Map(); rows.forEach((row) => { const sector = String(row.sector || "其他").trim() || "其他"; if (sector === "其他") return; counts.set(sector, (counts.get(sector) || 0) + 1); }); const cluster = [...counts.entries()].sort((left, right) => right[1] - left[1])[0]; const element = document.querySelector("#downSectorCluster"); element.hidden = !cluster || cluster[1] < 2; element.textContent = cluster && cluster[1] >= 2 ? `${cluster[0]}集中跌停 ×${cluster[1]}` : ""; } function changeDownSort(key) { if (state.downSortKey === key) state.downSortDirection = state.downSortDirection === "asc" ? "desc" : "asc"; else { state.downSortKey = key; state.downSortDirection = "asc"; } renderDownTable(state.dashboard?.down_limits || []); } function updateDownSortHeaders() { document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => { header.classList.remove("sort-asc", "sort-desc", "sorted"); header.setAttribute("aria-sort", "none"); if (header.dataset.downSort === state.downSortKey) { header.classList.add(state.downSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted"); header.setAttribute("aria-sort", state.downSortDirection === "asc" ? "ascending" : "descending"); } const arrow = header.querySelector(".arr"); if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.downSortDirection === "asc" ? "▲" : "▼") : "↕"; }); } function renderYesterdayTable(rows) { const visibleRows = getVisibleYesterdayRows(rows); const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value); const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || ""); setText("yesterdayCount", `${rows.length} 只`); setText("yesterdayMeta", ` · 昨日 ${previousDate} → 今日 ${currentDate}`); renderYesterdaySummary(rows); const body = document.querySelector("#yesterdayTableBody"); body.innerHTML = visibleRows.map((row, index) => ` ${index + 1} ${escapeHtml(row.name)}${escapeHtml(row.code)} ${number(row.prior_streak)} ${signed(row.current_change)} ${escapeHtml(row.outcome)} ${number(row.current_streak) ? `${number(row.current_streak)}` : ""} ${escapeHtml(row.sector || "其他")} ${escapeHtml(row.reason || "")} `).join(""); bindStockRows(body); document.querySelector("#yesterdayEmptyState").hidden = visibleRows.length !== 0; updateYesterdayControls(); } function getVisibleYesterdayRows(rows = state.dashboard?.yesterday_limits || []) { let visibleRows = rows.filter((row) => { if (state.yesterdayFilter === "advance") return row.outcome === "晋级"; if (state.yesterdayFilter === "positive") return number(row.current_change) > 0; if (state.yesterdayFilter === "fail") return row.outcome === "断板"; if (state.yesterdayFilter === "risk") return ["炸板", "跌停"].includes(row.outcome); return true; }); if (state.yesterdayQuery) { visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.yesterdayQuery)); } if (!state.yesterdaySortKey) return visibleRows; return visibleRows.sort((left, right) => { const result = number(left[state.yesterdaySortKey]) - number(right[state.yesterdaySortKey]); return state.yesterdaySortDirection === "asc" ? result : -result; }); } function renderYesterdaySummary(rows) { const total = rows.length; const advance = rows.filter((row) => row.outcome === "晋级").length; const positive = rows.filter((row) => number(row.current_change) > 0).length; const fail = rows.filter((row) => row.outcome === "断板").length; const risk = rows.filter((row) => ["炸板", "跌停"].includes(row.outcome)).length; const rate = (value) => total ? value / total * 100 : 0; setText("yesterdayAllCount", total); setText("yesterdayAdvanceCount", advance); setText("yesterdayAdvanceRate", `晋级率 ${formatNumber(rate(advance), 1)}%`); setText("yesterdayPositiveCount", positive); setText("yesterdayPositiveRate", `兑现率 ${formatNumber(rate(positive), 1)}%`); setText("yesterdayFailCount", fail); setText("yesterdayFailRate", `占 ${formatNumber(rate(fail), 1)}%`); setText("yesterdayRiskCount", risk); setText("yesterdayRiskRate", `亏钱效应 ${formatNumber(rate(risk), 1)}%`); } function yesterdayOutcomeClass(outcome) { return { "晋级": "advance", "断板": "fail", "炸板": "broken", "跌停": "down" }[outcome] || "fail"; } function changeYesterdaySort(key) { if (state.yesterdaySortKey === key) state.yesterdaySortDirection = state.yesterdaySortDirection === "asc" ? "desc" : "asc"; else { state.yesterdaySortKey = key; state.yesterdaySortDirection = "desc"; } renderYesterdayTable(state.dashboard?.yesterday_limits || []); } function updateYesterdayControls() { document.querySelectorAll("[data-yesterday-filter]").forEach((button) => { const active = button.dataset.yesterdayFilter === state.yesterdayFilter; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => { header.classList.remove("sort-asc", "sort-desc", "sorted"); header.setAttribute("aria-sort", "none"); if (header.dataset.yesterdaySort === state.yesterdaySortKey) { header.classList.add(state.yesterdaySortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted"); header.setAttribute("aria-sort", state.yesterdaySortDirection === "asc" ? "ascending" : "descending"); } const arrow = header.querySelector(".arr"); if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.yesterdaySortDirection === "asc" ? "▲" : "▼") : "↕"; }); } function renderPerformance(rows) { rows = normalizePerformanceRows(rows); const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value); const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || ""); setText("performanceDateRange", `昨日 ${previousDate} → 今日 ${currentDate}`); document.querySelector("#performanceCards").innerHTML = rows.map((row) => `
${escapeHtml(row.label)} → 今日${performanceRateState(row.advance_rate).label}
${formatNumber(row.advance_rate, 1)}% 晋级 ${number(row.advanced)} / 共 ${number(row.count)} 只
`).join("") || '
暂无昨日涨停统计
'; renderPerformanceConclusion(rows); renderMarketBreadth(state.dashboard?.overview || {}); } function normalizePerformanceRows(rows) { const groups = new Map(); (rows || []).forEach((row) => { const level = Math.max(1, number(row.level)); const displayLevel = Math.min(level, 5); const group = groups.get(displayLevel) || { level: displayLevel, label: displayLevel === 1 ? "昨日首板" : displayLevel === 5 ? "昨日5板+" : `昨日${displayLevel}板`, count: 0, advanced: 0, positive: 0, changeTotal: 0, }; const count = number(row.count); group.count += count; group.advanced += number(row.advanced); group.positive += count * number(row.positive_rate) / 100; group.changeTotal += count * number(row.average_change); groups.set(displayLevel, group); }); return [...groups.values()] .sort((left, right) => right.level - left.level) .map((group) => ({ level: group.level, label: group.label, count: group.count, advanced: group.advanced, advance_rate: group.count ? group.advanced / group.count * 100 : 0, positive_rate: group.count ? group.positive / group.count * 100 : 0, average_change: group.count ? group.changeTotal / group.count : 0, })); } function performanceRateState(rate) { const value = number(rate); if (value === 0) return { label: "失效", className: "is-neutral" }; if (value < 20) return { label: "危险", className: "is-warning" }; return { label: "活跃", className: "is-active" }; } function renderPerformanceConclusion(rows) { const container = document.querySelector("#performanceConclusion"); if (!rows.length) { container.innerHTML = '
暂无昨日梯队数据,暂不生成结论
'; return; } const sorted = [...rows].sort((left, right) => number(right.level) - number(left.level)); const highRows = sorted.filter((row) => number(row.level) >= 4); const highAdvanced = highRows.reduce((total, row) => total + number(row.advanced), 0); const highSamples = highRows.map((row) => escapeHtml(row.label)).join("、"); const strongest = [...rows].sort((left, right) => ( number(right.advance_rate) - number(left.advance_rate) || number(right.level) - number(left.level) ))[0]; const firstBoard = rows.find((row) => number(row.level) === 1); const overview = state.dashboard?.overview || {}; const phase = overview.sentiment_phase || "观察"; const up = number(overview.up_count); const down = number(overview.down_count); const breadthRate = up + down > 0 ? up / (up + down) * 100 : 50; const stance = breadthRate < 25 ? "宜守不宜攻" : breadthRate < 45 ? "控制仓位,聚焦核心" : "保持精选,跟随强势梯队"; const highText = highRows.length ? `高位晋级率${highAdvanced ? "仍有承接" : "全线失效"}:${highSamples}${highAdvanced ? `共晋级 ${highAdvanced} 只` : "今日均未晋级"};` : "高位梯队暂无昨日样本,空间信号仍待确认;"; const strongestText = strongest ? `${escapeHtml(strongest.label)}晋级率最高,为 ${formatNumber(strongest.advance_rate, 1)}%(${number(strongest.advanced)} 只晋级 / 共 ${number(strongest.count)} 只);` : "暂无相对占优梯队;"; const firstBoardText = firstBoard ? `首板基数 ${number(firstBoard.count)} 只,晋级率 ${formatNumber(firstBoard.advance_rate, 1)}%,低位接力${number(firstBoard.advance_rate) < 20 ? "胜率偏低" : "仍有活跃度"};` : "首板梯队暂无有效样本;"; container.innerHTML = `
· ${highText}
· ${strongestText}
· ${firstBoardText}
· 结论:${stance},当前情绪周期「${escapeHtml(phase)}」。
`; } /* PRESERVATION-SOURCE-END app.js:1517-1915 */