feat: complete heaven readings and screener publication

This commit is contained in:
leefer
2026-08-05 23:44:17 +08:00
parent 6a058c2929
commit b3a21d05b7
34 changed files with 3538 additions and 225 deletions
+218 -19
View File
@@ -49,7 +49,15 @@ function selectedScreenerResultKey(mode) {
function activeScreenerResultEntry(mode = state.screenerMode) {
const key = selectedScreenerResultKey(mode);
return key ? state.screenerResultStore[key] || null : null;
const stored = key ? state.screenerResultStore[key] || null : null;
if (stored) return stored;
const normalizedMode = ["smart", "curated", "quant"].includes(mode) ? mode : "smart";
const latest = state.screenerResults[normalizedMode];
const context = state.screenerResultContexts[normalizedMode];
if (!latest || !context) return null;
if (normalizedMode === "quant") return { result: latest, context };
const selected = currentScreenerStrategy(normalizedMode);
return selected?.name === context.strategyName ? { result: latest, context } : null;
}
function activeScreenerResult(mode = state.screenerMode) {
@@ -84,11 +92,10 @@ function setScreenerResult(mode, result, options = {}) {
function applyScreenerSetup(payload, requestKey) {
const dateChanged = Boolean(state.screenerSetupKey && state.screenerSetupKey !== requestKey);
if (dateChanged) {
state.screenerResults = { smart: null, curated: null, quant: null };
state.screenerResultContexts = { smart: null, curated: null, quant: null };
state.screenerResultStore = {};
}
state.screenerResults = { smart: null, curated: null, quant: null };
state.screenerResultContexts = { smart: null, curated: null, quant: null };
state.screenerResultStore = {};
if (dateChanged) state.screenerArchiveView = "latest";
state.screenerSetup = payload;
state.screenerSetupKey = requestKey;
@@ -182,7 +189,13 @@ async function loadScreenerSetup(force = false) {
function renderScreenerSetup() {
const setup = state.screenerSetup;
if (!setup) return;
setText("screenerDateLabel", `数据日期 ${displayCompactDate(setup.trade_date)}`);
const publishedDate = setup.published_batch?.trade_date;
setText(
"screenerDateLabel",
publishedDate && publishedDate !== setup.trade_date
? `数据日期 ${displayCompactDate(setup.trade_date)} · 候选批次 ${displayCompactDate(publishedDate)}`
: `数据日期 ${displayCompactDate(setup.trade_date)}`,
);
setText("regimeLabel", setup.regime.label);
setText("regimeConfidence", `置信度 ${formatNumber(setup.regime.confidence, 0)}%`);
setText("regimeStepStatus", `${setup.regime.label} · 置信度 ${formatNumber(setup.regime.confidence, 0)}%`);
@@ -233,6 +246,7 @@ function renderStrategySummary() {
function selectScreenerMode(mode) {
state.screenerMode = ["smart", "curated", "quant"].includes(mode) ? mode : "smart";
state.screenerArchiveView = "latest";
localStorage.setItem("xiaobaiScreenerMode", state.screenerMode);
state.screenerMobileView = "strategy";
renderScreenerMode();
@@ -293,6 +307,17 @@ function curatedSchoolIcon(school) {
}
function curatedStrategyRunState(strategy, result) {
const publishedRun = strategy?.published_run || {};
if (publishedRun.status === "missing_data") {
return { label: "数据不足", className: "missing", verifiedEmpty: false };
}
if (publishedRun.status === "no_signal") {
return { label: "暂无信号", className: "quiet", verifiedEmpty: true };
}
if (publishedRun.status === "ready") {
const count = (result?.candidates || []).length;
return { label: `${count} 只候选`, className: "ready", verifiedEmpty: false };
}
const missingData = strategy?.missing_data || [];
if (!strategy?.data_ready || missingData.length) {
return { label: "数据不足", className: "missing", verifiedEmpty: false };
@@ -397,15 +422,24 @@ function renderCuratedStrategyDetail() {
}
document.querySelector("#curatedHealthMetrics").innerHTML = [
["运行状态", statusLabel, statusClass],
["当日信号", result ? `${candidateCount}` : "--", ""],
["批次信号", result ? `${candidateCount}` : "--", ""],
["字段覆盖", health.coverage != null ? `${formatNumber(health.coverage, 1)}%` : strategy.data_ready ? "数据已就绪" : "--", ""],
["最近更新", updatedLabel, ""],
].map(([label, value, className]) => `<div><span>${escapeHtml(label)}</span><strong class="${className}">${escapeHtml(value)}</strong></div>`).join("");
const status = document.querySelector("#curatedDataStatus");
status.classList.toggle("missing", statusClass === "missing");
status.innerHTML = strategy.data_ready
? `<i data-lucide="${runState.verifiedEmpty ? "circle-check" : "database"}"></i><span><strong>${runState.verifiedEmpty ? "本日暂无信号" : "盘后自动更新"}</strong><small>${runState.verifiedEmpty ? `必需数据已完整,本日没有股票同时满足 ${filters.length} 项准入条件` : result ? health.required_field_count != null ? `已核验 ${number(health.required_field_count)} 项因子 · ${number(health.complete_rows)} 只股票` : "盘后定格结果已载入" : "等待当日行情定格后生成"}</small></span>`
: `<i data-lucide="circle-alert"></i><span><strong>数据尚未完备</strong><small>${escapeHtml((strategy.missing_data || []).join("、") || "等待后台同步")}</small></span>`;
const publishedRun = strategy.published_run || {};
if (publishedRun.status === "missing_data") {
status.innerHTML = `<i data-lucide="circle-alert"></i><span><strong>缺少必需数据</strong><small>${escapeHtml(publishedRun.detail || "等待后台同步")}</small></span>`;
} else if (publishedRun.status === "no_signal") {
status.innerHTML = `<i data-lucide="circle-check"></i><span><strong>数据完整,暂无信号</strong><small>必需数据已完整,本日没有股票同时满足 ${filters.length} 项准入条件</small></span>`;
} else if (result) {
status.innerHTML = `<i data-lucide="database"></i><span><strong>盘后批次已发布</strong><small>${health.required_field_count != null ? `已核验 ${number(health.required_field_count)} 项因子 · ${number(health.complete_rows)} 只股票` : "盘后定格结果已载入"}</small></span>`;
} else if (!strategy.data_ready) {
status.innerHTML = `<i data-lucide="circle-alert"></i><span><strong>数据尚未完备</strong><small>${escapeHtml((strategy.missing_data || []).join("、") || "等待后台同步")}</small></span>`;
} else {
status.innerHTML = "<i data-lucide=\"clock-3\"></i><span><strong>等待成功批次</strong><small>运行中或失败的批次不会覆盖上一成功结果</small></span>";
}
refreshIcons();
}
@@ -842,22 +876,163 @@ async function executeScreenerFormula({ mode, formula, strategyName, strategyId
}
function renderScreenerResult() {
renderScreenerArchiveTabs();
renderScreenerBatchNotice();
if (state.screenerArchiveView === "latest") {
renderLatestScreenerResult();
} else {
renderScreenerArchiveResult();
}
}
function renderScreenerArchiveTabs() {
document.querySelectorAll("[data-screener-archive-view]").forEach((button) => {
const active = button.dataset.screenerArchiveView === state.screenerArchiveView;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
});
}
function renderScreenerBatchNotice() {
const notice = document.querySelector("#screenerBatchNotice");
const setup = state.screenerSetup || {};
const batch = setup.published_batch;
const requestStatus = setup.automatic_status || {};
notice.classList.toggle("warning", Boolean(batch?.is_fallback) || ["failed", "partial"].includes(requestStatus.status));
if (state.screenerMode === "quant") {
notice.innerHTML = "<strong>手动执行</strong><span>自定义选股结果独立保存,不会覆盖阶段选股或策略选股。</span>";
notice.hidden = false;
return;
}
if (!batch) {
const statusText = {
running: "盘后候选正在生成,完成前不会发布局部结果。",
failed: "最近一次盘后任务失败,尚无可展示的成功批次。",
partial: "最近一次盘后任务未完整完成,尚无可展示的成功批次。",
pending: "盘后候选尚未生成。",
}[requestStatus.status] || "尚无成功发布的盘后候选批次。";
notice.innerHTML = `<strong>尚未发布</strong><span>${escapeHtml(statusText)}</span>`;
notice.hidden = false;
return;
}
const completed = number(batch.completed_count);
const skipped = number(batch.skipped_count);
const detail = batch.notice
|| `盘后候选已完整发布${completed ? ` · 完成 ${completed} 套策略` : ""}${skipped ? ` · ${skipped} 套因数据不足跳过` : ""}`;
const time = batch.finished_at ? ` · ${formatTimestamp(batch.finished_at)}` : "";
notice.innerHTML = `<strong>候选基准 ${escapeHtml(displayCompactDate(batch.trade_date))}${time}</strong><span>${escapeHtml(detail)}</span>`;
notice.hidden = false;
}
function selectedScreenerArchiveRows() {
const mode = state.screenerMode || "smart";
const source = state.screenerArchiveView === "active"
? state.screenerSetup?.active_signals || []
: state.screenerSetup?.candidate_history || [];
const strategy = currentScreenerStrategy(mode);
return source.filter((row) => {
if (row.mode !== mode) return false;
if (mode === "quant") return true;
return (row.hits || []).some((hit) => hit.strategy_name === strategy?.name);
}).map((row) => {
let hits = row.hits || [];
if (mode !== "quant") hits = hits.filter((hit) => hit.strategy_name === strategy?.name);
if (state.screenerArchiveView === "active") hits = hits.filter((hit) => hit.active);
const latestHit = [...hits].sort((left, right) => String(right.selection_date).localeCompare(String(left.selection_date)))[0] || {};
const active = hits.some((hit) => hit.active);
const labels = [...new Set(hits.map((hit) => hit.validity?.label).filter(Boolean))];
return {
...row,
selection_date: latestHit.selection_date || row.selection_date,
score_display: latestHit.score_display ?? row.score_display,
matched_strategies: [...new Set(hits.map((hit) => hit.strategy_name).filter(Boolean))],
status: active ? "持续有效" : "已到期",
active,
validity_label: labels.join(" / ") || row.validity_label || "--",
valid_until: latestHit.valid_until || "",
remaining_trading_days: latestHit.remaining_trading_days,
};
});
}
function renderScreenerArchiveResult() {
const mode = state.screenerMode || "smart";
const rows = selectedScreenerArchiveRows();
setText("screenerResultTitle", state.screenerArchiveView === "active" ? "持续有效信号" : "历史入选记录");
const latestTable = document.querySelector("#screenerLatestTable");
const archiveTable = document.querySelector("#screenerArchiveTable");
latestTable.hidden = true;
archiveTable.hidden = false;
setText("screenerResultCount", `${rows.length}`);
const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "自定义选股" };
const strategy = currentScreenerStrategy(mode);
const source = document.querySelector("#screenerResultSource");
source.textContent = [modeLabels[mode], strategy?.name].filter(Boolean).join(" · ");
source.hidden = false;
setText(
"screenerDisclaimer",
state.screenerArchiveView === "active"
? "持续有效按策略频率与阶段变化计算,不代表未来收益"
: "历史入选仅保留当时策略结果,不使用未来数据回写",
);
const body = document.querySelector("#screenerArchiveTableBody");
body.innerHTML = rows.map((row) => {
const validity = row.valid_until
? `${row.validity_label} · 至 ${displayCompactDate(row.valid_until)}`
: row.validity_label;
const matched = (row.matched_strategies || []).join("、");
return `<tr data-code="${escapeHtml(row.code)}">
<td class="num">${escapeHtml(displayCompactDate(row.selection_date))}</td>
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
<td>${escapeHtml(row.sector)}</td>
<td><span class="screener-signal-status ${row.active ? "active" : "expired"}">${escapeHtml(row.status)}</span></td>
<td class="screener-archive-strategies" title="${escapeHtml(matched)}">${escapeHtml(matched)}</td>
<td class="number num">${row.score_display == null ? "" : formatNumber(row.score_display, 1)}</td>
<td>${escapeHtml(validity)}</td>
</tr>`;
}).join("");
bindStockRows(body);
const empty = document.querySelector("#screenerEmpty");
empty.textContent = state.screenerArchiveView === "active"
? "当前策略暂无仍在有效期内的信号"
: "当前策略暂无历史入选记录";
empty.hidden = rows.length > 0;
renderBacktest(null);
updateBacktestTaskStatus();
}
function latestScreenerEmptyMessage(mode) {
if (mode === "quant") return "尚未执行自定义选股";
const strategy = currentScreenerStrategy(mode);
const publishedRun = strategy?.published_run || {};
if (publishedRun.status === "missing_data") {
return `缺少必需数据:${publishedRun.detail || "等待后台同步"}`;
}
if (publishedRun.status === "no_signal") return "数据完整,暂无符合条件个股";
const status = state.screenerSetup?.automatic_status?.status;
if (status === "running") return "盘后候选正在生成,成功发布前不会显示局部结果";
if (status === "failed") return "本次盘后任务失败,当前策略暂无已发布结果";
if (status === "partial") return "本次盘后任务未完整完成,当前策略暂无已发布结果";
return "尚无成功发布的盘后候选结果";
}
function renderLatestScreenerResult() {
const mode = state.screenerMode || "smart";
const titles = { smart: "盘后候选结果", curated: "策略候选结果", quant: "自定义选股结果" };
setText("screenerResultTitle", titles[mode]);
const result = activeScreenerResult(mode);
const context = activeScreenerResultContext(mode);
const source = document.querySelector("#screenerResultSource");
const emptyMessages = {
smart: "当日盘后候选尚未生成",
curated: "所选策略的当日候选尚未生成",
quant: "尚未执行自定义选股",
};
const emptyMessage = latestScreenerEmptyMessage(mode);
document.querySelector("#screenerLatestTable").hidden = false;
document.querySelector("#screenerArchiveTable").hidden = true;
if (!result) {
setText("screenerResultCount", "0 只");
source.hidden = true;
source.textContent = "";
setText("screenerDisclaimer", "历史统计不代表未来收益");
document.querySelector("#screenerTableBody").innerHTML = "";
document.querySelector("#screenerEmpty").textContent = emptyMessages[mode];
document.querySelector("#screenerEmpty").textContent = emptyMessage;
document.querySelector("#screenerEmpty").hidden = false;
renderBacktest(null);
if (mode === "smart") setText("screenerRunStatus", "等待执行");
@@ -881,7 +1056,10 @@ function renderScreenerResult() {
: `盘后数据 ${displayCompactDate(meta.trade_date)} · ${result.disclaimer}`,
);
const empty = document.querySelector("#screenerEmpty");
empty.textContent = mode === "curated" ? "暂无符合条件个股" : emptyMessages[mode];
const verifiedEmpty = mode === "curated" ? "暂无符合条件个股" : "数据完整,暂无符合条件个股";
empty.textContent = mode === "quant"
? emptyMessage
: `${verifiedEmpty}${mode === "curated" ? "(必需数据已完整)" : ""}`;
empty.hidden = candidates.length > 0;
const body = document.querySelector("#screenerTableBody");
const runId = number(meta.run_id);
@@ -1053,7 +1231,20 @@ function parseFormulaEditor() {
}
function exportScreenerResults() {
const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "量化选股" };
const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "自定义选股" };
if (state.screenerArchiveView !== "latest") {
const viewLabel = state.screenerArchiveView === "active" ? "持续有效" : "入选历史";
const rows = selectedScreenerArchiveRows().map((row) => ({
...row,
matched_strategy_names: (row.matched_strategies || []).join("、"),
}));
exportRows(`${modeLabels[state.screenerMode]}-${viewLabel}`, rows, [
["入选日", "selection_date"], ["股票代码", "code"], ["股票名称", "name"],
["板块", "sector"], ["状态", "status"], ["命中策略", "matched_strategy_names"],
["最新评分", "score_display"], ["有效期", "validity_label"],
]);
return;
}
exportRows(modeLabels[state.screenerMode] || "智能选股", activeScreenerResult()?.candidates || [], [
["股票代码", "code"], ["股票名称", "name"], ["板块", "sector"], ["综合分", "score_display"],
["历史条件估计%", "historical_probability"], ["当日涨幅%", "pct_chg"], ["5日涨幅%", "return_5d"],
@@ -1091,6 +1282,14 @@ function bindScreenerEvents() {
document.querySelectorAll("[data-screener-mode]").forEach((button) => {
button.addEventListener("click", () => selectScreenerMode(button.dataset.screenerMode));
});
document.querySelectorAll("[data-screener-archive-view]").forEach((button) => {
button.addEventListener("click", () => {
state.screenerArchiveView = ["active", "history"].includes(button.dataset.screenerArchiveView)
? button.dataset.screenerArchiveView
: "latest";
renderScreenerResult();
});
});
document.querySelector("#curatedStrategyList").addEventListener("click", (event) => {
if (event.target.closest("button")) return;
const card = event.target.closest("[data-curated-strategy]");