feat: track screener candidates across five trading days

This commit is contained in:
leefer
2026-07-22 23:55:33 +08:00
parent 50dbb8a3ff
commit 5ae2791dd3
9 changed files with 510 additions and 1 deletions
+76
View File
@@ -43,6 +43,7 @@ const state = {
selectedRegime: "",
selectedStrategy: null,
screenerResult: null,
screenerTracking: null,
screenerMobileView: "strategy",
sentimentHistory: null,
sentimentRange: 20,
@@ -485,6 +486,7 @@ function bindEvents() {
document.querySelector("#backfillButton").addEventListener("click", backfillData);
document.querySelector("#factorSyncButton").addEventListener("click", syncFactorData);
document.querySelector("#screenerRunButton").addEventListener("click", runScreener);
document.querySelector("#refreshTrackingButton").addEventListener("click", refreshScreenerTracking);
document.querySelectorAll("[data-screener-mobile-view]").forEach((button) => {
button.addEventListener("click", () => selectScreenerMobileView(button.dataset.screenerMobileView));
});
@@ -1590,6 +1592,7 @@ async function loadScreenerSetup() {
|| null;
renderScreenerSetup();
if (state.screenerResult) renderScreenerResult();
await loadScreenerTracking();
} catch (error) {
showToast(error.message || "选股配置加载失败");
}
@@ -1805,6 +1808,7 @@ async function runScreener() {
});
state.screenerResult = payload.result;
renderScreenerResult();
await loadScreenerTracking(true);
setText("screenerRunStatus", `完成 · ${payload.result.candidates.length}`);
updateBacktestTaskStatus();
if (window.innerWidth <= 720) selectScreenerMobileView("results");
@@ -3557,6 +3561,78 @@ function renderScreenerResult() {
updateBacktestTaskStatus();
}
async function loadScreenerTracking(force = false) {
if (state.screenerTracking && !force) {
renderScreenerTracking();
return;
}
try {
state.screenerTracking = await apiRequest("/api/screener/tracking?limit=12");
renderScreenerTracking();
} catch (error) {
showToast(error.message || "策略跟踪加载失败");
}
}
async function refreshScreenerTracking() {
const button = document.querySelector("#refreshTrackingButton");
button.disabled = true;
setStatus("正在更新策略跟踪");
try {
const payload = await apiRequest("/api/screener/tracking/refresh", "POST", {
trade_date: elements.tradeDate.value,
});
state.screenerTracking = payload.tracking;
renderScreenerTracking();
if (payload.notice) showToast(payload.notice);
setStatus("策略跟踪已更新");
} catch (error) {
showToast(error.message || "策略跟踪刷新失败");
setStatus("策略跟踪刷新失败");
} finally {
button.disabled = false;
}
}
function renderScreenerTracking() {
const payload = state.screenerTracking || { batches: [], summary: {} };
const batches = payload.batches || [];
const rows = batches.flatMap((batch) => (batch.items || []).map((item) => ({
...item,
selection_date: batch.selection_date,
strategy_name: batch.strategy_name,
})));
setText("trackingBatchCount", `${batches.length}`);
const summary = payload.summary || {};
document.querySelector("#trackingSummary").innerHTML = [
["跟踪标的", `${number(summary.total)}`],
["已有 T+1", `${number(summary.observed)}`],
["T+1 胜率", trackingPercent(summary.t1_win_rate)],
["T+5 胜率", trackingPercent(summary.t5_win_rate)],
["T+5 平均", trackingReturn(summary.average_t5)],
].map(([label, value]) => `<div><span>${label}</span><strong>${value}</strong></div>`).join("");
document.querySelector("#trackingEmpty").hidden = rows.length > 0;
document.querySelector("#trackingTableBody").innerHTML = rows.map((row) => `
<tr data-code="${escapeHtml(row.code)}">
<td>${displayCompactDate(row.selection_date)}</td>
<td class="tracking-strategy" title="${escapeHtml(row.strategy_name)}">${escapeHtml(row.strategy_name)}</td>
<td><span class="stock-cell"><strong>${escapeHtml(row.name)}</strong><small class="stock-code">${escapeHtml(row.code)}</small></span></td>
<td class="number">${formatNumber(row.entry_price, 2)}</td>
${["t1_open", "t1_close", "t3_close", "t5_close", "max_gain", "max_drawdown"].map((key) => `<td class="number ${row[key] == null ? "" : changeClass(row[key])}">${trackingReturn(row[key])}</td>`).join("")}
<td><span class="tracking-status ${row.status === "已完成" ? "complete" : row.observed_days ? "active" : "pending"}">${escapeHtml(row.status)}</span></td>
</tr>
`).join("");
bindStockRows(document.querySelector("#trackingTableBody"));
}
function trackingReturn(value) {
return value == null ? "--" : `${signed(value)}%`;
}
function trackingPercent(value) {
return value == null ? "--" : `${formatNumber(value, 1)}%`;
}
function renderBacktest(backtest) {
const panel = document.querySelector("#backtestPanel");
panel.hidden = !backtest;