diff --git a/app/frontend/m/js/pages.js b/app/frontend/m/js/pages.js index b7d94e3..ece020d 100644 --- a/app/frontend/m/js/pages.js +++ b/app/frontend/m/js/pages.js @@ -1653,33 +1653,60 @@ return null; } - // 持续有效 / 入选历史:对齐电脑端 selectedScreenerArchiveRows,按当前策略过滤 active_signals/candidate_history 组装行 + // 持续有效 / 入选历史:生产 /api/screener/setup 只返回 recent_results / latest_results, + // 从 recent_results 里筛出当前策略(meta.strategy_name 匹配,与 screenerRunForCurrentStrategy 同口径)的运行,逐候选组装。 function screenerArchiveRows(payload, view) { if (!payload) return []; - const source = view === "active" ? (payload.active_signals || []) : (payload.candidate_history || []); const strategy = screenerCurrentStrategy(payload); const strategyName = strategy ? strategy.name : ""; - const mode = screenerLibraryMode(screenerCurrentLibrary(payload)); - return source.filter(function (row) { - if ((row.mode || "smart") !== mode) return false; - return (row.hits || []).some(function (hit) { return hit.strategy_name === strategyName; }); - }).map(function (row) { - let hits = (row.hits || []).filter(function (hit) { return hit.strategy_name === strategyName; }); - if (view === "active") hits = hits.filter(function (hit) { return hit.active; }); - const latestHit = hits.slice().sort(function (a, b) { - return String(b.selection_date || "").localeCompare(String(a.selection_date || "")); - })[0] || {}; - return { - code: row.code, - name: row.name, - sector: row.sector, - pct_chg: row.pct_chg, - return_5d: row.return_5d, - score_display: latestHit.score_display != null ? latestHit.score_display : row.score_display, - selection_date: displayCompactDate(latestHit.selection_date || row.selection_date || ""), - status: view === "active" ? "持续有效" : "已入选" - }; + if (!strategyName) return []; + const rows = []; + (payload.recent_results || []).forEach(function (run) { + if (!run || !run.meta || run.meta.strategy_name !== strategyName) return; + const meta = run.meta; + const active = screenerSignalActive(run, payload); + if (view === "active" && !active) return; + (run.candidates || []).forEach(function (c) { + const code = String(c.code || ""); + if (!code) return; + rows.push({ + code: code, + name: c.name || "", + sector: c.sector || "", + pct_chg: c.pct_chg, + return_5d: c.return_5d, + volume_ratio_5d: c.volume_ratio_5d, + sector_strength: c.sector_strength, + historical_probability: c.historical_probability, + score_display: c.score_display, + selection_date: displayCompactDate(String(meta.trade_date || "")) + }); + }); }); + rows.sort(function (a, b) { + return String(b.selection_date || "").localeCompare(String(a.selection_date || "")); + }); + return rows; + } + + // 信号是否仍在有效期:阶段/精选策略按「阶段不变即有效」(meta.regime == 当前阶段); + // 量化策略按策略频率对应的交易日数,以 meta.trade_date 与数据日比较(无交易日历时按自然日近似)。 + function screenerSignalActive(run, payload) { + const meta = (run && run.meta) || {}; + const mode = String(meta.mode || "smart"); + if (mode === "smart" || mode === "curated") { + const regime = payload && payload.regime ? String(payload.regime.id || "") : ""; + return regime ? String(meta.regime || "") === regime : true; + } + const freq = String((((run && run.formula) || {}).meta || {}).frequency || "每日"); + const days = { "每周": 5, "双周": 10, "月度": 20, "事件驱动": 5 }[freq] || 1; + const selected = String(meta.trade_date || "").replaceAll("-", ""); + const asOf = String((payload && payload.trade_date) || "").replaceAll("-", ""); + if (!selected || !asOf || selected === asOf) return true; + const start = parseLocalDate(displayCompactDate(selected)); + const end = parseLocalDate(displayCompactDate(asOf)); + const diff = Math.round((end.getTime() - start.getTime()) / 86400000); + return diff >= 0 && diff < days; } function screenerViewKey(payload, view) { @@ -1861,12 +1888,12 @@ } function screenerEmptyText(payload) { - if (state.screener.view === "active") return "该策略暂无持续有效信号"; - if (state.screener.view === "history") return "该策略暂无入选历史"; const status = screenerStrategyStatusText(payload); if (status === "数据不足") return "该策略数据不足,暂无法出候选"; if (status === "等待盘后") return "该策略等待盘后更新"; if (status === "选择策略") return "请先选择一个策略查看结果"; + if (state.screener.view === "active") return "该策略暂无持续有效信号"; + if (state.screener.view === "history") return "该策略暂无入选历史"; return "该策略当日暂无信号"; }