feat: track screener candidates across five trading days
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user