feat: expand screeners and stabilize interactive feedback

This commit is contained in:
leefer
2026-07-28 22:47:50 +08:00
parent f4b2d7152a
commit 1cc80583b3
22 changed files with 2707 additions and 509 deletions
+257 -207
View File
@@ -53,6 +53,10 @@ const state = {
rotationHistory: null,
rotationHistoryKey: "",
rotationSelectedSector: "",
rotationSelectedDate: "",
rotationMembers: null,
rotationMembersKey: "",
rotationMembersLoading: false,
rotationOrder: localStorage.getItem("xiaobaiRotationOrder") === "latest" ? "latest" : "oldest",
rotationLoading: false,
auctionData: null,
@@ -104,6 +108,7 @@ const state = {
screenerSetupPromise: null,
selectedRegime: "",
selectedStrategy: null,
customStrategyDraft: null,
screenerRunning: false,
screenerRunningMode: "",
screenerResults: { smart: null, curated: null, quant: null },
@@ -114,7 +119,9 @@ const state = {
? localStorage.getItem("xiaobaiScreenerMode")
: "smart",
curatedCategory: "全部",
curatedSchool: "全部",
curatedQuery: "",
curatedViewMode: localStorage.getItem("xiaobaiCuratedViewMode") === "grid" ? "grid" : "list",
selectedCuratedStrategyId: 0,
quantFilters: [],
quantScores: [],
@@ -519,8 +526,6 @@ async function applyAuthenticatedSession(session) {
document.querySelector("#syncButton").hidden = !isAdmin;
document.querySelector("#reasonForm").hidden = !isAdmin;
document.querySelector("#sectorPhaseManager").hidden = !isAdmin;
const factorSyncButton = document.querySelector("#factorSyncButton");
if (factorSyncButton) factorSyncButton.hidden = false;
document.querySelector("#authGate").hidden = true;
applyMembershipAccess();
await startAuthenticatedApp();
@@ -815,8 +820,7 @@ function bindEvents() {
}
renderAuctionTable();
});
document.querySelector("#changeStrategyButton").addEventListener("click", () => openStrategyDrawer("library"));
document.querySelector("#openStrategyDrawerButton").addEventListener("click", () => openStrategyDrawer("editor"));
document.querySelector("#openStrategyDrawerButton").addEventListener("click", openCustomStrategyDrawer);
document.querySelector("#closeStrategyDrawerButton").addEventListener("click", () => document.querySelector("#strategyDrawer").close());
document.querySelector("#strategyDrawer").addEventListener("click", (event) => {
if (event.target === event.currentTarget) event.currentTarget.close();
@@ -911,8 +915,6 @@ function bindEvents() {
document.querySelector("#stockReminderButton").addEventListener("click", openStockReminder);
document.querySelector("#reasonForm").addEventListener("submit", saveReasonOverride);
document.querySelector("#backfillButton").addEventListener("click", backfillData);
document.querySelector("#factorSyncButton").addEventListener("click", syncFactorData);
document.querySelector("#screenerRunButton").addEventListener("click", runScreener);
document.querySelector("#openScreenerTrackingButton").addEventListener("click", async () => {
await loadScreenerTracking(true);
openView("screenerTrackingView");
@@ -943,11 +945,22 @@ function bindEvents() {
state.curatedQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
renderCuratedStrategyLibrary();
});
document.querySelector("#curatedRunButton").addEventListener("click", runCuratedStrategy);
document.querySelector("#curatedBacktestToggle").addEventListener("change", updateBacktestTaskStatus);
document.querySelector("#closeCuratedDetailButton").addEventListener("click", () => document.querySelector("#curatedDetailDialog").close());
document.querySelector("#curatedDetailDialog").addEventListener("click", (event) => {
if (event.target === event.currentTarget) event.currentTarget.close();
document.querySelector("#curatedCategoryFilter").addEventListener("change", (event) => {
state.curatedCategory = event.target.value;
renderCuratedStrategyLibrary();
});
document.querySelector("#curatedSchoolFilters").addEventListener("click", (event) => {
const button = event.target.closest("[data-curated-school]");
if (!button) return;
state.curatedSchool = button.dataset.curatedSchool;
renderCuratedStrategyLibrary();
});
document.querySelectorAll("[data-curated-view]").forEach((button) => {
button.addEventListener("click", () => {
state.curatedViewMode = button.dataset.curatedView === "grid" ? "grid" : "list";
localStorage.setItem("xiaobaiCuratedViewMode", state.curatedViewMode);
renderCuratedStrategyLibrary();
});
});
document.querySelector("#quantResetButton").addEventListener("click", resetQuantBuilder);
document.querySelector("#addQuantFilterButton").addEventListener("click", () => addQuantFilter());
@@ -1161,7 +1174,7 @@ function renderDashboard() {
renderYesterdayTable(state.dashboard.yesterday_limits || []);
renderPerformance(state.dashboard.limit_performance || []);
renderLadderBoard(ladders || []);
renderRotationTable(state.dashboard.sector_rotation || [], sectors || []);
renderRotationMembers();
}
async function loadSentimentHistory(force = false) {
@@ -1787,6 +1800,7 @@ function updateYesterdayControls() {
}
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}`);
@@ -1803,6 +1817,39 @@ function renderPerformance(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" };
@@ -1956,8 +2003,9 @@ function renderRotationHistory() {
<button class="rotation-track-cancel" type="button">取消追踪</button>`;
tracker.querySelector(".rotation-track-cancel").addEventListener("click", () => {
state.rotationSelectedSector = "";
state.rotationSelectedDate = "";
renderRotationHistory();
updateRotationTableSelection();
loadRotationMembers("");
});
} else {
tracker.hidden = true;
@@ -1974,7 +2022,7 @@ function renderRotationHistory() {
const strength = clamp(number(sector.strength), 0, 100);
const heatClass = strength >= 90 ? "heat-strong" : strength >= 70 ? "heat-warm" : "heat-mild";
return `
<button type="button" class="rotation-sector-chip ${heatClass} ${selected === sector.name ? "selected" : ""}" data-rotation-sector="${escapeHtml(sector.name)}">
<button type="button" class="rotation-sector-chip ${heatClass} ${selected === sector.name ? "selected" : ""}" data-rotation-sector="${escapeHtml(sector.name)}" data-rotation-date="${escapeHtml(day.trade_date)}">
<span class="rotation-rank rank-${Math.min(number(sector.rank), 4)}">${number(sector.rank)}</span><strong>${escapeHtml(sector.name)}</strong><small><b>${number(sector.count)}</b> · ${formatNumber(sector.strength, 0)}</small>
<span class="rotation-cell-tooltip">${escapeHtml(displayCompactDate(day.trade_date).slice(5))} · ${number(sector.rank)} · 涨停 ${number(sector.count)} · 强度 ${formatNumber(sector.strength, 0)}</span>
</button>`;
@@ -1983,58 +2031,76 @@ function renderRotationHistory() {
}).join("");
container.querySelectorAll("[data-rotation-sector]").forEach((button) => {
button.addEventListener("click", () => {
state.rotationSelectedSector = button.dataset.rotationSector === state.rotationSelectedSector
? ""
: button.dataset.rotationSector;
const clickedSector = button.dataset.rotationSector;
const clickedDate = button.dataset.rotationDate;
const isSameSelection = clickedSector === state.rotationSelectedSector
&& clickedDate === state.rotationSelectedDate;
state.rotationSelectedSector = isSameSelection ? "" : clickedSector;
state.rotationSelectedDate = isSameSelection ? "" : clickedDate;
renderRotationHistory();
updateRotationTableSelection();
loadRotationMembers(state.rotationSelectedSector);
});
});
}
function renderRotationTable(rows, sectors) {
const sectorMap = new Map(sectors.map((sector) => [sector.name, sector]));
async function loadRotationMembers(sector, force = false) {
if (!sector) {
state.rotationMembers = null;
state.rotationMembersKey = "";
renderRotationMembers();
return;
}
const memberDate = state.rotationSelectedDate || elements.tradeDate.value;
const key = `${memberDate}:${sector}`;
if (!force && state.rotationMembersKey === key && state.rotationMembers) {
renderRotationMembers();
return;
}
state.rotationMembersLoading = true;
renderRotationMembers();
try {
const query = new URLSearchParams({ trade_date: memberDate, sector });
state.rotationMembers = await apiRequest(`/api/rotation/members?${query}`);
state.rotationMembersKey = key;
} catch (error) {
state.rotationMembers = { error: error.message || "成分股加载失败", rows: [] };
state.rotationMembersKey = key;
} finally {
state.rotationMembersLoading = false;
renderRotationMembers();
}
}
function renderRotationMembers() {
const body = document.querySelector("#rotationTableBody");
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
setText("rotationDetailMeta", `${currentDate} · ${rows.length} 个板块`);
body.innerHTML = rows.map((row) => {
const sector = sectorMap.get(row.name) || {};
const strength = number(row.strength ?? sector.strength);
const currentCount = number(row.count);
const previousCount = number(row.previous_count);
const delta = row.delta == null ? currentCount - previousCount : number(row.delta);
const trend = previousCount === 0 && currentCount > 0
? "新进"
: currentCount > previousCount
? "升温"
: currentCount < previousCount ? "降温" : "持平";
return `
<tr class="rotation-detail-row ${state.rotationSelectedSector === row.name ? "selected" : ""}" data-rotation-detail-sector="${escapeHtml(row.name)}"><td class="number">${number(row.rank)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
<td><span class="trend-tag ${trendClass(trend)}">${trend}</span></td>
<td class="number up">${currentCount}</td><td class="number muted">${previousCount}</td>
<td class="number ${delta >= 0 ? "delta-positive" : "delta-negative"}" data-sort-value="${delta}">${delta > 0 ? "+" : ""}${delta}</td>
<td data-sort-value="${strength}"><div class="rotation-strength"><span class="strength-cell"><i style="width:${clamp(strength, 0, 100)}%"></i></span><b>${formatNumber(strength, 0)}</b></div></td>
<td class="number streak-value">${streakLabel(row.max_streak || 1)}</td>
<td class="number ${changeClass(sector.change)}" data-sort-value="${number(sector.change)}">${signed(sector.change)}%</td>
<td>${escapeHtml(row.leader || sector.leader || "--")}</td><td class="number" data-sort-value="${number(row.amount_billion)}">${formatNumber(row.amount_billion, 1)} 亿</td></tr>
`;
}).join("");
body.querySelectorAll("[data-rotation-detail-sector]").forEach((row) => {
row.addEventListener("click", () => {
const sector = row.dataset.rotationDetailSector;
state.rotationSelectedSector = state.rotationSelectedSector === sector ? "" : sector;
renderRotationHistory();
updateRotationTableSelection();
document.querySelector("#rotationHistory").scrollIntoView({ behavior: "smooth", block: "center" });
});
});
const empty = document.querySelector("#rotationMembersEmpty");
if (state.rotationMembersLoading) {
body.innerHTML = "";
empty.textContent = `正在核验${state.rotationSelectedSector}成分股`;
empty.hidden = false;
return;
}
const payload = state.rotationMembers;
const rows = payload?.rows || [];
if (!state.rotationSelectedSector || !payload || payload.error || !rows.length) {
body.innerHTML = "";
empty.textContent = payload?.error || (state.rotationSelectedSector ? "该板块暂无可用成分行情" : "点击上方任意板块查看成分股");
empty.hidden = false;
setText("rotationDetailTitle", "板块成分股");
setText("rotationDetailMeta", state.rotationSelectedSector || "--");
return;
}
empty.hidden = true;
setText("rotationDetailTitle", `${payload.meta?.sector_name || state.rotationSelectedSector}成分股`);
setText("rotationDetailMeta", `${displayCompactDate(payload.meta?.trade_date)} · ${number(payload.meta?.quoted_count)} / ${number(payload.meta?.member_count)}`);
body.innerHTML = rows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"><td class="number num muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
<td class="number num ${row.quoted ? changeClass(row.change) : "muted"}" data-sort-value="${row.quoted ? number(row.change) : -999}">${row.quoted ? signed(row.change) : ""}</td>
<td class="number num">${row.quoted ? formatNumber(row.open, 2) : ""}</td><td class="number num">${row.quoted ? formatNumber(row.close, 2) : ""}</td>
<td class="number num" data-sort-value="${number(row.amount_billion)}">${row.quoted ? formatNumber(row.amount_billion, 2) : ""}</td><td>${row.quoted ? "" : ""}</td></tr>
`).join("");
animateRows(body);
}
function updateRotationTableSelection() {
document.querySelectorAll("#rotationTableBody [data-rotation-detail-sector]").forEach((row) => {
row.classList.toggle("selected", row.dataset.rotationDetailSector === state.rotationSelectedSector);
});
bindStockRows(body);
}
function renderLadderMini(ladders) {
@@ -3498,12 +3564,7 @@ function applyScreenerSetup(payload, requestKey) {
if (!latestResults.smart && payload.latest_result) latestResults.smart = payload.latest_result;
const smartLatestMeta = latestResults.smart?.meta || {};
const curatedLatestMeta = latestResults.curated?.meta || {};
const availableRegimes = new Set((payload.regimes || []).map((item) => item.id));
if (!state.selectedRegime || dateChanged) {
state.selectedRegime = availableRegimes.has(smartLatestMeta.regime)
? smartLatestMeta.regime
: payload.regime.id;
}
state.selectedRegime = payload.regime.id;
const selectedId = state.selectedStrategy?.id;
const smartStrategies = payload.strategies.filter((item) => item.formula?.meta?.library !== "curated");
@@ -3617,13 +3678,9 @@ function renderScreenerSetup() {
const selector = document.querySelector("#regimeSelector");
selector.innerHTML = setup.regimes.map((item) => `
<button type="button" class="regime-option ${item.id === state.selectedRegime ? "active" : ""}" data-regime="${item.id}">${escapeHtml(item.label)}</button>
<span class="regime-option ${item.id === state.selectedRegime ? "active" : ""}">${escapeHtml(item.label)}</span>
`).join("");
selector.querySelectorAll("[data-regime]").forEach((button) => {
button.addEventListener("click", () => selectRegime(button.dataset.regime));
});
renderStrategyList();
populateStrategyEditor(state.selectedStrategy);
renderStrategySummary();
renderScreenerMode();
renderCuratedStrategyLibrary();
@@ -3663,7 +3720,7 @@ function renderScreenerMode() {
const results = document.querySelector("#screenerView .screener-results-view");
const resultsSlot = document.querySelector(`[data-screener-results-slot="${mode}"]`);
if (results && resultsSlot && results.parentElement !== resultsSlot) resultsSlot.append(results);
const titles = { smart: "候选结果", curated: "执行结果", quant: "打分结果" };
const titles = { smart: "盘后候选结果", curated: "策略候选结果", quant: "自定义选股结果" };
setText("screenerResultTitle", titles[mode]);
renderScreenerResult();
}
@@ -3677,56 +3734,83 @@ function activeCuratedStrategy() {
return strategies.find((item) => item.id === state.selectedCuratedStrategyId) || strategies[0] || null;
}
function curatedStrategySchool(strategy) {
const category = String(strategy?.formula?.meta?.category || "");
if (["红利价值", "质量价值", "现金流价值", "成长质量", "小盘质量"].includes(category)) return "基本面";
if (["行业轮动", "形态突破", "趋势追踪"].includes(category)) return "趋势";
if (["短线竞价", "连板接力", "低吸反核"].includes(category)) return "短线";
if (["动量反转"].includes(category)) return "动量";
if (/红利|价值|质量|成长|财务|现金流/.test(category)) return "基本面";
if (/趋势|轮动|突破/.test(category)) return "趋势";
if (/竞价|连板|龙头|反核|首阴|反包|打板/.test(category)) return "短线";
if (/动量|反转/.test(category)) return "动量";
return "其他";
}
function curatedSchoolIcon(school) {
return { 基本面: "circle-dollar-sign", 趋势: "trending-up", 短线: "zap", 动量: "refresh-cw", 其他: "boxes" }[school] || "boxes";
}
function curatedStrategyRunState(strategy, result) {
const missingData = strategy?.missing_data || [];
if (!strategy?.data_ready || missingData.length) {
return { label: "数据不足", className: "missing", verifiedEmpty: false };
}
if (!result) return { label: "等待盘后", className: "pending", verifiedEmpty: false };
const count = (result.candidates || []).length;
if (count) return { label: `${count} 只候选`, className: "ready", verifiedEmpty: false };
return { label: "暂无信号", className: "quiet", verifiedEmpty: true };
}
function renderCuratedStrategyLibrary() {
if (!state.screenerSetup) return;
const strategies = curatedStrategies();
const categories = ["全部", ...new Set(strategies.map((item) => item.formula?.meta?.category || "其他"))];
const schools = ["全部", "基本面", "趋势", "短线", "动量"];
if (!categories.includes(state.curatedCategory)) state.curatedCategory = "全部";
if (!schools.includes(state.curatedSchool)) state.curatedSchool = "全部";
setText("curatedStrategyCount", `${strategies.length}`);
const filters = document.querySelector("#curatedCategoryFilters");
filters.innerHTML = categories.map((category) => `
<button class="${category === state.curatedCategory ? "active" : ""}" type="button" data-curated-category="${escapeHtml(category)}">${escapeHtml(category)}</button>
const categorySelect = document.querySelector("#curatedCategoryFilter");
categorySelect.innerHTML = categories.map((category) => `
<option value="${escapeHtml(category)}" ${category === state.curatedCategory ? "selected" : ""}>${escapeHtml(category)}</option>
`).join("");
filters.querySelectorAll("[data-curated-category]").forEach((button) => {
button.addEventListener("click", () => {
state.curatedCategory = button.dataset.curatedCategory;
renderCuratedStrategyLibrary();
});
document.querySelector("#curatedSchoolFilters").innerHTML = schools.map((school) => {
const count = school === "全部" ? strategies.length : strategies.filter((item) => curatedStrategySchool(item) === school).length;
return `<button class="${school === state.curatedSchool ? "active" : ""}" type="button" data-curated-school="${escapeHtml(school)}" aria-pressed="${school === state.curatedSchool}">${escapeHtml(school)}<small>${count}</small></button>`;
}).join("");
document.querySelectorAll("[data-curated-view]").forEach((button) => {
const active = button.dataset.curatedView === state.curatedViewMode;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
const query = state.curatedQuery;
const visible = strategies.filter((item) => {
const meta = item.formula?.meta || {};
const categoryMatch = state.curatedCategory === "全部" || meta.category === state.curatedCategory;
const queryMatch = !query || `${item.name} ${item.description} ${meta.category}`.toLocaleLowerCase("zh-CN").includes(query);
return categoryMatch && queryMatch;
const school = curatedStrategySchool(item);
const schoolMatch = state.curatedSchool === "全部" || school === state.curatedSchool;
const queryMatch = !query || `${item.name} ${item.description} ${meta.category} ${school} ${meta.suitable_environment} ${meta.failure_risk}`.toLocaleLowerCase("zh-CN").includes(query);
return categoryMatch && schoolMatch && queryMatch;
});
const list = document.querySelector("#curatedStrategyList");
list.classList.toggle("is-grid", state.curatedViewMode === "grid");
list.innerHTML = visible.length ? visible.map((strategy) => {
const meta = strategy.formula?.meta || {};
const school = curatedStrategySchool(strategy);
const rank = strategies.findIndex((item) => item.id === strategy.id) + 1;
const resultKey = screenerResultKey(screenerResultContext("curated", null, {
regime: strategy.regimes[0] || state.selectedRegime,
strategyId: strategy.id,
strategyName: strategy.name,
}));
const result = state.screenerResultStore[resultKey]?.result;
const runState = curatedStrategyRunState(strategy, result);
return `<article class="curated-strategy-card ${strategy.id === activeCuratedStrategy()?.id ? "active" : ""}" data-curated-strategy="${strategy.id}">
<span class="curated-card-head"><i class="curated-strategy-rank">${String(rank).padStart(2, "0")}</i><span><strong>${escapeHtml(strategy.name)}</strong><small>${escapeHtml(meta.category || "")}</small></span><i class="curated-ready-dot ${strategy.data_ready ? "" : "missing"}" title="${strategy.data_ready ? "" : escapeHtml((strategy.missing_data || []).join(""))}"></i></span>
<span class="curated-card-tags"><em>质量 ${escapeHtml(meta.quality || "--")}</em><em>${escapeHtml(meta.frequency || "--")}</em><em> ${escapeHtml(meta.risk || "--")}</em></span>
<span class="curated-card-description">${escapeHtml(strategy.description || "查看策略条件与适用环境。")}</span>
<span class="curated-card-foot"><small>${strategy.data_ready ? "数据已就绪" : `缺少 ${(strategy.missing_data || []).length} 项数据`}</small><span class="curated-card-actions"><button type="button" data-curated-inspect="${strategy.id}"></button><button type="button" class="primary" data-curated-run="${strategy.id}" ${strategy.data_ready ? "" : "disabled"}><i data-lucide="play"></i></button></span></span>
<span class="curated-strategy-icon" aria-hidden="true"><i data-lucide="${curatedSchoolIcon(school)}"></i></span>
<span class="curated-card-head"><i class="curated-strategy-rank">${String(rank).padStart(2, "0")}</i><span><strong>${escapeHtml(strategy.name)}</strong><small>${escapeHtml(school)} · ${escapeHtml(meta.category || "")}</small></span><em class="curated-card-result ${runState.className}">${escapeHtml(runState.label)}</em></span>
<span class="curated-card-tags"><em>${escapeHtml(meta.quality || "--")}</em><em>${escapeHtml(meta.frequency || "--")}</em><em> ${escapeHtml(meta.risk || "--")}</em></span>
</article>`;
}).join("") : '<div class="empty-state">没有符合条件的策略</div>';
list.querySelectorAll("[data-curated-inspect]").forEach((button) => {
button.addEventListener("click", () => {
state.selectedCuratedStrategyId = number(button.dataset.curatedInspect);
renderCuratedStrategyLibrary();
renderScreenerResult();
document.querySelector("#curatedDetailDialog").showModal();
});
});
list.querySelectorAll("[data-curated-run]").forEach((button) => {
button.addEventListener("click", () => {
state.selectedCuratedStrategyId = number(button.dataset.curatedRun);
renderCuratedStrategyLibrary();
renderScreenerResult();
runCuratedStrategy();
});
});
renderCuratedStrategyDetail();
}
@@ -3735,6 +3819,10 @@ function renderCuratedStrategyDetail() {
if (!strategy) return;
const formula = strategy.formula || {};
const meta = formula.meta || {};
const result = activeScreenerResult("curated");
const resultMeta = result?.meta || {};
const health = resultMeta.health || {};
const runState = curatedStrategyRunState(strategy, result);
setText("curatedStrategyCategory", meta.category || "精选策略");
setText("curatedStrategyName", strategy.name);
setText("curatedStrategyDescription", strategy.description);
@@ -3742,6 +3830,8 @@ function renderCuratedStrategyDetail() {
`质量 ${meta.quality || "--"}`, meta.frequency || "--", `风险 ${meta.risk || "--"}`,
meta.data_group || "行情因子",
].map((value) => `<span>${escapeHtml(value)}</span>`).join("");
setText("curatedSuitableEnvironment", meta.suitable_environment || "以策略条件为准");
setText("curatedFailureRisk", meta.failure_risk || "策略可能随市场结构变化而失效");
const filters = formula.filters || [];
setText("curatedFilterCount", `${filters.length}`);
document.querySelector("#curatedFilterList").innerHTML = filters.map((item) => `
@@ -3754,14 +3844,27 @@ function renderCuratedStrategyDetail() {
const percent = number(item.weight) / total * 100;
return `<div class="curated-score-row"><span>${escapeHtml(factorLabel(item.field))}</span><span class="curated-score-track"><i style="width:${Math.min(100, percent)}%"></i></span><strong>${formatNumber(percent, 0)}%</strong></div>`;
}).join("");
const candidateCount = (result?.candidates || []).length;
const statusLabel = runState.className === "ready" ? "运行正常" : runState.label;
const statusClass = runState.className;
let updatedLabel = "--";
if (resultMeta.updated_at) {
const updated = new Date(resultMeta.updated_at);
if (!Number.isNaN(updated.getTime())) {
updatedLabel = `${String(updated.getMonth() + 1).padStart(2, "0")}-${String(updated.getDate()).padStart(2, "0")} ${updated.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false })}`;
}
}
document.querySelector("#curatedHealthMetrics").innerHTML = [
["运行状态", statusLabel, statusClass],
["当日信号", 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", !strategy.data_ready);
status.classList.toggle("missing", statusClass === "missing");
status.innerHTML = strategy.data_ready
? '<i data-lucide="database"></i><span><strong>策略数据已就绪</strong><small>可按当前数据日期执行</small></span>'
: `<i data-lucide="circle-alert"></i><span><strong>需要补充数据</strong><small>${escapeHtml((strategy.missing_data || []).join("、") || "请同步因子")}</small></span>`;
const runButton = document.querySelector("#curatedRunButton");
runButton.disabled = !strategy.data_ready;
runButton.title = strategy.data_ready ? "执行当前策略" : `缺少${(strategy.missing_data || []).join("、")}`;
? `<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>`;
refreshIcons();
}
@@ -3973,6 +4076,22 @@ function openStrategyDrawer(target = "editor") {
});
}
function openCustomStrategyDrawer() {
if (!state.customStrategyDraft) {
state.customStrategyDraft = {
id: null,
builtin: false,
name: "自定义选股策略",
description: "",
regimes: [state.selectedRegime],
formula: buildQuantFormula(),
};
}
populateStrategyEditor(state.customStrategyDraft);
renderStrategyList();
openStrategyDrawer("editor");
}
function selectScreenerMobileView(view) {
state.screenerMobileView = view === "results" ? "results" : "strategy";
const workspace = document.querySelector("#screenerView");
@@ -3986,11 +4105,7 @@ function selectScreenerMobileView(view) {
}
function updateBacktestTaskStatus() {
const enabled = document.querySelector("#runBacktestToggle")?.checked;
const backtest = activeScreenerResult("smart")?.backtest;
setText("backtestTaskStatus", state.screenerRunning && state.screenerRunningMode === "smart" && enabled
? "正在回测"
: backtest ? `已完成 · ${number(backtest.samples)} 样本` : enabled ? "随选股执行" : "本次不执行");
setText("backtestTaskStatus", activeScreenerResult("smart") ? "结果已归档" : "等待盘后生成");
renderScreenerProgress();
}
@@ -4003,18 +4118,18 @@ function selectRegime(regime) {
function renderStrategyList() {
const list = document.querySelector("#strategyList");
const strategies = state.screenerSetup.strategies.filter((item) => item.formula?.meta?.library !== "curated");
const strategies = state.screenerSetup.strategies.filter((item) => !item.builtin && item.formula?.meta?.library !== "curated");
list.innerHTML = strategies.map((strategy) => `
<button type="button" class="strategy-item ${strategy.id === state.selectedStrategy?.id ? "active" : ""}" data-strategy-id="${strategy.id}">
<button type="button" class="strategy-item ${strategy.id === state.customStrategyDraft?.id ? "active" : ""}" data-strategy-id="${strategy.id}">
<strong>${escapeHtml(strategy.name)}</strong><span>${escapeHtml(strategy.description || "--")}</span>
<small>${strategy.regimes.map((item) => regimeLabel(item)).join(" / ")}${strategy.builtin ? " · 内置" : ""}</small>
<small>${strategy.regimes.map((item) => regimeLabel(item)).join(" / ")}</small>
</button>
`).join("");
`).join("") || '<div class="empty-state">暂无已保存的自定义公式</div>';
list.querySelectorAll("[data-strategy-id]").forEach((button) => {
button.addEventListener("click", () => {
state.selectedStrategy = state.screenerSetup.strategies.find((item) => item.id === number(button.dataset.strategyId));
state.selectedRegime = state.selectedStrategy.regimes[0] || state.selectedRegime;
renderScreenerSetup();
state.customStrategyDraft = state.screenerSetup.strategies.find((item) => item.id === number(button.dataset.strategyId));
populateStrategyEditor(state.customStrategyDraft);
renderStrategyList();
});
});
}
@@ -4033,32 +4148,6 @@ function populateStrategyEditor(strategy) {
document.querySelector("#formulaEditor").value = JSON.stringify(strategy.formula, null, 2);
}
async function syncFactorData() {
const button = document.querySelector("#factorSyncButton");
button.disabled = true;
setText("factorTaskStatus", "同步中");
setLoading(true, "正在同步 45 个交易日因子数据");
setStatus("正在同步选股因子");
try {
const payload = await apiRequest("/api/screener/sync", "POST", {
trade_date: elements.tradeDate.value,
lookback: 45,
});
const result = payload.result;
showToast(`因子同步完成:${result.calendar_dates} 个交易日,竞价覆盖 ${number(result.auction_dates)}`);
await loadScreenerSetup(true);
setText("factorTaskStatus", `已就绪 · ${number(result.calendar_dates)}`);
setStatus("选股因子已同步");
} catch (error) {
showToast(error.message);
setStatus("选股因子同步失败");
setText("factorTaskStatus", "同步失败");
} finally {
setLoading(false);
button.disabled = false;
}
}
async function compileStrategy() {
const prompt = document.querySelector("#strategyPrompt").value.trim();
const button = document.querySelector("#compileStrategyButton");
@@ -4071,9 +4160,8 @@ async function compileStrategy() {
regime: state.selectedRegime,
});
const strategy = payload.strategy;
state.selectedStrategy = { ...strategy, id: null, builtin: false };
state.customStrategyDraft = { ...strategy, id: null, builtin: false };
document.querySelector("#deleteStrategyButton").hidden = true;
renderStrategySummary();
document.querySelector("#strategyNameInput").value = strategy.name;
document.querySelector("#strategyDescriptionInput").value = strategy.description;
document.querySelector("#formulaEditor").value = JSON.stringify(strategy.formula, null, 2);
@@ -4099,8 +4187,9 @@ async function saveCurrentStrategy() {
formula,
});
state.screenerSetup.strategies = payload.strategies;
state.selectedStrategy = payload.strategies.find((item) => item.id === payload.id);
renderScreenerSetup();
state.customStrategyDraft = payload.strategies.find((item) => item.id === payload.id);
renderStrategyList();
populateStrategyEditor(state.customStrategyDraft);
showToast("自定义策略已保存");
} catch (error) {
showToast(error.message);
@@ -4108,7 +4197,7 @@ async function saveCurrentStrategy() {
}
async function deleteCurrentStrategy() {
const strategy = state.selectedStrategy;
const strategy = state.customStrategyDraft;
if (!strategy?.id || strategy.builtin) {
showToast("只能删除已保存的自定义策略");
return;
@@ -4120,10 +4209,9 @@ async function deleteCurrentStrategy() {
try {
const payload = await apiRequest(`/api/screener/strategies/${strategy.id}`, "DELETE");
state.screenerSetup.strategies = payload.strategies;
state.selectedStrategy = payload.strategies.find((item) => item.formula?.meta?.library !== "curated" && item.regimes.includes(state.selectedRegime))
|| payload.strategies.find((item) => item.formula?.meta?.library !== "curated")
|| null;
renderScreenerSetup();
state.customStrategyDraft = null;
renderStrategyList();
openCustomStrategyDrawer();
showToast("自定义策略已删除");
} catch (error) {
showToast(error.message || "策略删除失败");
@@ -4132,26 +4220,6 @@ async function deleteCurrentStrategy() {
}
}
async function runCuratedStrategy() {
const strategy = activeCuratedStrategy();
if (!strategy) return;
if (!strategy.data_ready) {
showToast(`请先同步${(strategy.missing_data || []).join("、")}`);
return;
}
const regime = strategy.regimes.includes(state.selectedRegime) ? state.selectedRegime : strategy.regimes[0];
await executeScreenerFormula({
mode: "curated",
formula: strategy.formula,
strategyName: strategy.name,
strategyId: strategy.id,
regime,
runBacktest: document.querySelector("#curatedBacktestToggle").checked,
button: document.querySelector("#curatedRunButton"),
loadingText: `正在执行“${strategy.name}”并计算历史样本`,
});
}
async function runQuantStrategy() {
let formula;
try {
@@ -4163,27 +4231,27 @@ async function runQuantStrategy() {
await executeScreenerFormula({
mode: "quant",
formula,
strategyName: "自定义量化公式",
strategyName: "自定义选股公式",
regime: state.selectedRegime,
runBacktest: document.querySelector("#quantBacktestToggle").checked,
button: document.querySelector("#quantRunButton"),
loadingText: "正在执行量化公式并计算因子贡献",
loadingText: "正在执行自定义公式并计算因子贡献",
});
}
function saveQuantAsStrategy() {
try {
const formula = buildQuantFormula();
state.selectedStrategy = {
state.customStrategyDraft = {
id: null,
builtin: false,
name: "自定义量化策略",
description: "由量化因子工作台生成,可在高级公式中继续调整。",
name: "自定义选股策略",
description: "由自定义因子工作台生成,可在高级公式中继续调整。",
regimes: [state.selectedRegime],
formula,
};
populateStrategyEditor(state.selectedStrategy);
document.querySelector("#strategyPrompt").value = "量化因子工作台生成的自定义公式";
populateStrategyEditor(state.customStrategyDraft);
document.querySelector("#strategyPrompt").value = "自定义因子工作台生成的选股公式";
openStrategyDrawer("editor");
} catch (error) {
showToast(error.message);
@@ -4239,26 +4307,6 @@ async function executeScreenerFormula({ mode, formula, strategyName, strategyId
}
}
async function runScreener() {
let formula;
try {
formula = parseFormulaEditor();
} catch (error) {
showToast(error.message);
return;
}
await executeScreenerFormula({
mode: "smart",
formula,
strategyName: document.querySelector("#strategyNameInput").value,
strategyId: state.selectedStrategy?.id,
regime: state.selectedRegime,
runBacktest: document.querySelector("#runBacktestToggle").checked,
button: document.querySelector("#screenerRunButton"),
loadingText: "正在计算因子排名与滚动回测",
});
}
async function loadMentorSetup(force = false) {
const requestedDate = elements.tradeDate.value.replaceAll("-", "");
if (!force && state.mentorSetup?.requestedDate === requestedDate) {
@@ -6619,9 +6667,9 @@ function renderScreenerResult() {
const context = activeScreenerResultContext(mode);
const source = document.querySelector("#screenerResultSource");
const emptyMessages = {
smart: "尚未执行当前阶段与策略的选股",
curated: "尚未执行所选策略",
quant: "尚未执行量化选股",
smart: "当日盘后候选尚未生成",
curated: "所选策略的当日候选尚未生成",
quant: "尚未执行自定义选股",
};
if (!result) {
setText("screenerResultCount", "0 只");
@@ -6639,9 +6687,9 @@ function renderScreenerResult() {
}
const candidates = result.candidates || [];
setText("screenerResultCount", `${candidates.length}`);
const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "量化选股" };
const modeLabels = { smart: "阶段选股", curated: "策略选股", quant: "自定义选股" };
const sourceParts = [modeLabels[mode]];
if (mode !== "quant" && context?.regime) sourceParts.push(regimeLabel(context.regime));
if (mode === "smart" && context?.regime) sourceParts.push(regimeLabel(context.regime));
sourceParts.push(mode === "quant" ? "自定义因子权重" : context?.strategyName || result.meta?.strategy_name || "未命名策略");
source.textContent = sourceParts.join(" · ");
source.hidden = false;
@@ -6652,7 +6700,9 @@ function renderScreenerResult() {
? `盘中行情 · 历史样本截至 ${displayCompactDate(meta.history_cutoff)} · ${result.disclaimer}`
: `盘后数据 ${displayCompactDate(meta.trade_date)} · ${result.disclaimer}`,
);
document.querySelector("#screenerEmpty").hidden = candidates.length > 0;
const empty = document.querySelector("#screenerEmpty");
empty.textContent = mode === "curated" ? "暂无符合条件个股" : emptyMessages[mode];
empty.hidden = candidates.length > 0;
const body = document.querySelector("#screenerTableBody");
const runId = number(meta.run_id);
body.innerHTML = candidates.map((row, index) => `
-5
View File
@@ -219,11 +219,6 @@ body.drawer-open .drawer-mask{display:block}
.field textarea{min-height:90px;resize:vertical}
.field input:focus,.field textarea:focus{border-color:var(--blue-line)}
/* toast */
.toast{position:fixed;top:60px;left:50%;transform:translateX(-50%);background:var(--ink);color:#fff;
padding:8px 18px;border-radius:8px;font-size:12.5px;z-index:200;opacity:0;transition:opacity .2s;pointer-events:none}
.toast.show{opacity:.95}
/* ========== 集合竞价页 ========== */
.auc-head{display:flex;align-items:center;gap:12px;margin-bottom:12px;flex-wrap:wrap}
.auc-head h2{font-size:17px;font-weight:800}
+67 -57
View File
@@ -19,9 +19,9 @@
</script>
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/renovation.css?v=20260725-5">
<link rel="stylesheet" href="/redesign-v2.css?v=20260726-34">
<link rel="stylesheet" href="/redesign-v2.css?v=20260728-1">
<link rel="stylesheet" href="/design-system.css?v=20260728-4">
<link rel="stylesheet" href="/theme.css?v=20260728-1">
<link rel="stylesheet" href="/theme.css?v=20260728-2">
<link rel="stylesheet" href="/wentian-v2.css?v=20260728-7">
</head>
<body>
@@ -449,12 +449,12 @@
<div class="workspace-heading card-h redesigned-card-head"><h3 id="sentimentStageGuideTitle">判定口径</h3><span>温度 + 结构共同判定</span></div>
<div class="sentiment-stage-guide-head" aria-hidden="true"><span>阶段</span><span>典型特征</span><span>温度区间</span><span>策略取向</span></div>
<div class="sentiment-stage-guide-grid">
<article data-sentiment-stage="冰点"><strong>冰点</strong><span>涨停稀少、跌停成堆、高度压至 2 板</span><span class="stage-range">0-20</span><small>抗跌先手,允许无结果</small></article>
<article data-sentiment-stage="修复"><strong>修复</strong><span>跌停减少、首板增多、出现反包</span><span class="stage-range">20-40</span><small>修复先锋,小仓试错</small></article>
<article data-sentiment-stage="发酵"><strong>发酵</strong><span>主线清晰、梯队成型、晋级率走高</span><span class="stage-range">40-60</span><small>主线跟随</small></article>
<article data-sentiment-stage="高潮"><strong>高潮</strong><span>涨停扩散、空间打开、情绪充沛</span><span class="stage-range">60-85</span><small>核心去后排</small></article>
<article data-sentiment-stage="分化"><strong>分化</strong><span>高低切换、炸板增多、主线内部分歧</span><span class="stage-range">45-65</span><small>承接回流</small></article>
<article data-sentiment-stage="退潮"><strong>退潮</strong><span>高度压缩、晋级走低、亏钱效应扩散</span><span class="stage-range">20-40 且下降</span><small>防守观察</small></article>
<article data-sentiment-stage="冰点"><strong>冰点</strong><span>涨停稀少、跌停成堆、高度显著压缩</span><span class="stage-range">低于 25</span><small>抗跌先手,允许无结果</small></article>
<article data-sentiment-stage="修复"><strong>修复</strong><span>风险收敛、温度从低位有效回升</span><span class="stage-range">25+ 且回升</span><small>修复先锋,小仓试错</small></article>
<article data-sentiment-stage="发酵"><strong>发酵</strong><span>主线清晰、梯队成型、连续转强</span><span class="stage-range">45+ 且连续确认</span><small>主线跟随</small></article>
<article data-sentiment-stage="高潮"><strong>高潮</strong><span>温度、赚钱效应与涨停生态共振</span><span class="stage-range">80+ 且生态达标</span><small>核心去后排</small></article>
<article data-sentiment-stage="分化"><strong>分化</strong><span>高低切换、炸板增多、主线内部分歧</span><span class="stage-range">45+ 且结构转弱</span><small>承接回流</small></article>
<article data-sentiment-stage="退潮"><strong>退潮</strong><span>温度或系统健康度继续走弱</span><span class="stage-range">低于 45 且走弱</span><small>防守观察</small></article>
</div>
</section>
</div>
@@ -801,7 +801,7 @@
<div class="screener-mode-tabs method" role="tablist" aria-label="智能选股模式">
<button class="active" type="button" role="tab" aria-selected="true" data-screener-mode="smart">阶段选股</button>
<button type="button" role="tab" aria-selected="false" data-screener-mode="curated">策略选股</button>
<button type="button" role="tab" aria-selected="false" data-screener-mode="quant">量化选股</button>
<button type="button" role="tab" aria-selected="false" data-screener-mode="quant">自定义选股</button>
</div>
<button id="openScreenerTrackingButton" class="button screener-tracking-entry" type="button"><i data-lucide="chart-no-axes-combined"></i>策略跟踪</button>
</div>
@@ -831,8 +831,8 @@
</div>
<div id="regimeReason" class="regime-advice">--</div>
<div class="regime-control-line">
<div id="regimeSelector" class="regime-selector" role="group" aria-label="选择市场阶段"></div>
<div class="factor-data-status"><span>跟随自动识别(点击可手动覆盖)</span><strong id="factorDateCount">0 日</strong><small id="factorDateRange">尚未同步</small></div>
<div id="regimeSelector" class="regime-selector" aria-label="系统识别的市场阶段"></div>
<div class="factor-data-status"><span>盘后行情定格后自动更新</span><strong id="factorDateCount">0 日</strong><small id="factorDateRange">等待后台数据</small></div>
</div>
</div>
</div>
@@ -842,17 +842,12 @@
<div class="screener-strategy-summary">
<div class="screener-strategy-title"><strong id="activeStrategyHeading">--</strong><span id="activeStrategyRegimes"></span></div>
<p id="activeStrategyDescription">等待匹配当前市场阶段的策略。</p>
<div class="screener-strategy-actions">
<button id="changeStrategyButton" class="button" type="button">更换策略</button>
<button id="openStrategyDrawerButton" class="button" type="button">编辑 / 自定义策略</button>
</div>
<div class="screener-strategy-actions"><span class="screener-auto-note">由系统按当前阶段自动匹配</span></div>
</div>
</section>
</div>
<div class="screener-runbar runbar">
<div class="screener-run-actions">
<button id="screenerRunButton" class="button primary" type="button"><i data-lucide="play"></i>执行选股</button>
<button id="factorSyncButton" class="button" type="button">同步因子数据</button>
<button id="screenerExportButton" class="button" type="button">导出 CSV</button>
</div>
<div class="screener-pipeline-status" aria-live="polite">
@@ -863,37 +858,52 @@
<div data-screener-results-slot="smart"></div>
</div>
<div class="curated-screener-panel" data-screener-panel="curated" hidden>
<section class="curated-library-pane" aria-label="精选策略库">
<div class="curated-library-heading">
<div><span>策略库</span><h3>选择策略并直接执行</h3><p>适用阶段仅作参考,策略条件与权重可在详情中查看。</p></div><strong id="curatedStrategyCount">10 套</strong>
</div>
<div class="curated-library-controls">
<label class="curated-search"><i data-lucide="search"></i><span class="visually-hidden">搜索策略</span><input id="curatedStrategySearch" type="search" placeholder="搜索名称或类别" autocomplete="off"></label>
<div id="curatedCategoryFilters" class="curated-category-filters" role="group" aria-label="策略分类"></div>
</div>
<div id="curatedStrategyList" class="curated-strategy-list"></div>
</section>
<dialog id="curatedDetailDialog" class="curated-detail-dialog" aria-labelledby="curatedStrategyName">
<section class="curated-detail-pane">
<button id="closeCuratedDetailButton" class="icon-button curated-detail-close" type="button" title="关闭" aria-label="关闭策略条件"><i data-lucide="x"></i></button>
<header class="curated-detail-header">
<div><span id="curatedStrategyCategory">精选策略</span><h3 id="curatedStrategyName">选择一套策略</h3><p id="curatedStrategyDescription">查看策略条件、数据状态和适用环境。</p></div>
<div id="curatedStrategyBadges" class="curated-strategy-badges"></div>
</header>
<div class="curated-detail-grid">
<section class="curated-condition-section"><div class="mini-section-heading"><h4>准入条件</h4><span id="curatedFilterCount">0 项</span></div><div id="curatedFilterList" class="curated-rule-list"></div></section>
<section class="curated-condition-section"><div class="mini-section-heading"><h4>评分权重</h4><span id="curatedWeightTotal">100%</span></div><div id="curatedScoreList" class="curated-score-list"></div></section>
</div>
<div class="curated-execution-bar">
<div id="curatedDataStatus" class="curated-data-status"><i data-lucide="database"></i><span><strong>检查数据中</strong><small>同步后显示可用状态</small></span></div>
<label class="checkbox-control"><input id="curatedBacktestToggle" type="checkbox" checked>滚动回测</label>
<button id="curatedRunButton" class="button primary" type="button"><i data-lucide="play"></i>执行该策略</button>
</div>
</section>
</dialog>
<div class="curated-workspace">
<aside class="curated-library-pane" aria-label="精选策略库">
<div class="curated-library-heading">
<div><span>策略库</span><h3>盘后自动候选池</h3></div><strong id="curatedStrategyCount">10 套</strong>
</div>
<div class="curated-library-controls">
<label class="curated-search"><i data-lucide="search"></i><span class="visually-hidden">搜索策略</span><input id="curatedStrategySearch" type="search" placeholder="搜索策略" autocomplete="off"></label>
<label class="curated-category-select"><span class="visually-hidden">策略分类</span><select id="curatedCategoryFilter" aria-label="策略分类"></select><i data-lucide="chevron-down"></i></label>
<div class="curated-view-toggle" role="group" aria-label="策略排列方式">
<button class="active" type="button" data-curated-view="list" aria-label="列表排列" title="列表排列" aria-pressed="true"><i data-lucide="list"></i></button>
<button type="button" data-curated-view="grid" aria-label="图标排列" title="图标排列" aria-pressed="false"><i data-lucide="layout-grid"></i></button>
</div>
</div>
<div id="curatedSchoolFilters" class="curated-school-filters" aria-label="策略流派"></div>
<div id="curatedStrategyList" class="curated-strategy-list"></div>
</aside>
<section class="curated-detail-pane" aria-labelledby="curatedStrategyName">
<header class="curated-detail-header">
<div><span id="curatedStrategyCategory">精选策略</span><h3 id="curatedStrategyName">选择一套策略</h3><p id="curatedStrategyDescription">查看策略条件、数据状态和适用环境。</p></div>
<div id="curatedStrategyBadges" class="curated-strategy-badges"></div>
</header>
<div class="curated-environment-notes">
<p><strong>适用环境</strong><span id="curatedSuitableEnvironment"></span></p>
<p><strong>失效风险</strong><span id="curatedFailureRisk"></span></p>
</div>
<div class="curated-health-grid" id="curatedHealthMetrics" aria-label="策略运行状态"></div>
<div class="curated-detail-grid">
<section class="curated-condition-section"><div class="mini-section-heading"><h4>准入条件</h4><span id="curatedFilterCount">0 项</span></div><div id="curatedFilterList" class="curated-rule-list"></div></section>
<section class="curated-condition-section"><div class="mini-section-heading"><h4>评分权重</h4><span id="curatedWeightTotal">100%</span></div><div id="curatedScoreList" class="curated-score-list"></div></section>
</div>
<div class="curated-execution-bar">
<div id="curatedDataStatus" class="curated-data-status"><i data-lucide="database"></i><span><strong>检查数据中</strong><small>同步后显示可用状态</small></span></div>
<span class="screener-auto-note">候选池由后台盘后自动更新</span>
</div>
</section>
</div>
<div data-screener-results-slot="curated"></div>
</div>
<div class="quant-screener-panel" data-screener-panel="quant" hidden>
<section class="custom-screener-tools card">
<div><span>自定义能力</span><strong>用自然语言生成公式,或直接编辑受控公式</strong><small>自定义选股仅在点击执行后计算,不影响系统盘后候选池。</small></div>
<div>
<button id="openStrategyDrawerButton" class="button" type="button"><i data-lucide="sparkles"></i>自然语言生成公式</button>
<button id="quantSaveButton" class="button" type="button"><i data-lucide="braces"></i>编辑 / 保存公式</button>
</div>
</section>
<section class="quant-builder-pane">
<header class="quant-panel-heading"><h3>因子与权重</h3><div><button id="addQuantScoreButton" class="button" type="button"><i data-lucide="plus"></i>添加因子</button><button id="quantResetButton" class="button ghost" type="button"><i data-lucide="rotate-ccw"></i>重置</button></div></header>
<div id="quantScoreRows" class="quant-rule-rows"></div>
@@ -912,16 +922,15 @@
</div>
<div id="quantFilterRows" class="quant-rule-rows"></div>
<div class="quant-execution-actions">
<button id="quantRunButton" class="button primary" type="button"><i data-lucide="play"></i>执行量化选股</button>
<button id="quantSaveButton" class="button" type="button"><i data-lucide="save"></i>保存为方案</button>
<button id="quantRunButton" class="button primary" type="button"><i data-lucide="play"></i>执行自定义选股</button>
<label class="checkbox-control"><input id="quantBacktestToggle" type="checkbox" checked>滚动回测</label>
<span>结果按加权总分排序,并生成逐股贡献解释</span>
</div>
<p id="quantValidationMessage" class="quant-validation-message" role="status" aria-live="polite"></p>
</div>
</aside>
<div data-screener-results-slot="quant"></div>
</div>
<div class="custom-results-slot" data-screener-results-slot="quant"></div>
</div>
<div class="screener-results-view">
<section id="backtestPanel" class="backtest-panel screener-backtest-strip" hidden>
@@ -935,6 +944,7 @@
</div>
<div class="table-frame card tbl-wrap screener-result-frame">
<table class="data-table tbl">
<colgroup class="screener-result-columns"><col><col><col><col><col><col><col><col><col><col><col><col></colgroup>
<thead><tr>
<th class="num">排名</th><th>股票</th><th>板块</th><th class="number num sortable">综合分<span class="arr"></span></th>
<th class="number num sortable">历史估计(%<span class="arr"></span></th><th class="number num sortable">当日涨幅(%<span class="arr"></span></th><th class="number num sortable">5日涨幅(%<span class="arr"></span></th>
@@ -946,7 +956,7 @@
</div>
</div>
<dialog id="strategyDrawer" class="strategy-drawer" aria-labelledby="strategyDrawerTitle">
<div class="strategy-drawer-header"><div><span>阶段选股</span><h2 id="strategyDrawerTitle">编辑 / 自定义策略</h2></div><button id="closeStrategyDrawerButton" class="icon-button" type="button" aria-label="关闭策略编辑"><i data-lucide="x"></i></button></div>
<div class="strategy-drawer-header"><div><span>自定义选股</span><h2 id="strategyDrawerTitle">自然语言与受控公式</h2></div><button id="closeStrategyDrawerButton" class="icon-button" type="button" aria-label="关闭策略编辑"><i data-lucide="x"></i></button></div>
<div class="strategy-drawer-body">
<aside class="strategy-sidebar">
<div class="workspace-heading card-h"><h3>策略库</h3><span id="strategyCount">0 套</span></div>
@@ -1108,22 +1118,22 @@
<section class="rotation-detail-card card" aria-labelledby="rotationDetailTitle">
<header class="rotation-card-head card-h">
<div>
<h3 id="rotationDetailTitle">当日轮动明细</h3>
<span>趋势、强度与涨停梯队 · 点击行可联动追踪上方轨迹</span>
<h3 id="rotationDetailTitle">板块成分股</h3>
<span>点击上方板块,查看目标交易日有效申万成分与行情</span>
</div>
<span id="rotationDetailMeta" class="rotation-top-tag">--</span>
</header>
<div class="rotation-table-frame tbl-wrap">
<table id="rotationTable" class="data-table tbl rotation-table">
<thead><tr>
<th class="number num">排名</th><th>板块</th><th>趋势</th><th class="number num">今日涨停(只)</th>
<th class="number num">昨日涨停(只)</th><th class="number num" data-auto-sort="true" title="变化:点击排序">变化(只</th>
<th class="number num" data-auto-sort="true" title="强度:点击排序">强度(分</th><th class="number num">最高板(板</th>
<th class="number num" data-auto-sort="true" title="平均涨幅:点击排序">平均涨幅(%</th><th>领涨股</th>
<th class="number num" data-auto-sort="true" title="涨停股成交额:点击排序">涨停股成交额(亿)</th>
<th class="number num">序号</th><th>代码</th><th>股票</th>
<th class="number num" data-auto-sort="true" title="涨跌幅:点击排序">涨跌幅(%</th>
<th class="number num">开盘价(元</th><th class="number num">收盘价(元</th>
<th class="number num" data-auto-sort="true" title="成交额:点击排序">成交额(亿</th><th>行情状态</th>
</tr></thead>
<tbody id="rotationTableBody"></tbody>
</table>
<div id="rotationMembersEmpty" class="empty-state">点击上方任意板块查看成分股</div>
</div>
</section>
</section>
@@ -1873,6 +1883,6 @@
<script src="/vendor/lucide.min.js" defer></script>
<script src="/ui-core.js" defer></script>
<script src="/heaven-loading-v2.js?v=20260728-2" defer></script>
<script src="/app.js?v=20260728-2" defer></script>
<script src="/app.js?v=20260728-3" defer></script>
</body>
</html>
+357 -54
View File
@@ -616,7 +616,7 @@ body.sidebar-collapsed .status-bar { left: 64px; }
.sentiment-current-phase-badge strong { display: block; color: var(--r2-up); font-size: 19px; font-weight: 800; line-height: 1.35; }
.sentiment-current-phase-badge span { display: block; margin-top: 2px; color: var(--r2-sub); font-size: 11px; white-space: nowrap; }
.sentiment-phase-info { min-width: 0; flex: 1; }
.sentiment-phase-info p { color: #374151; font-size: 12.5px; line-height: 1.7; }
.sentiment-phase-info p { color: var(--r2-sub); font-size: 12.5px; line-height: 1.7; }
.sentiment-phase-info p b { font-weight: 700; }
.sentiment-phase-info p .down { color: var(--r2-down); }
.sentiment-phase-info p .up { color: var(--r2-up); }
@@ -1050,7 +1050,7 @@ body.sidebar-collapsed .status-bar { left: 64px; }
.pool-side-group p {
margin: 0;
overflow: hidden;
color: #4b5563;
color: var(--r2-sub);
font-size: 12px;
line-height: 1.8;
text-overflow: ellipsis;
@@ -1819,7 +1819,7 @@ body.sidebar-collapsed .status-bar { left: 64px; }
.performance-conclusion {
padding: 14px 16px;
color: #374151;
color: var(--r2-sub);
font-size: 12.5px;
line-height: 2;
}
@@ -5222,38 +5222,114 @@ body.sidebar-collapsed .status-bar { left: 64px; }
/* Curated strategy workspace. */
#screenerView .curated-screener-panel { display: block; }
#screenerView .curated-library-pane { padding: 0; border: 0; background: transparent; box-shadow: none; }
#screenerView .curated-workspace {
display: grid;
grid-template-columns: minmax(340px, .9fr) minmax(560px, 1.5fr);
gap: 12px;
align-items: stretch;
}
#screenerView .curated-library-pane,
#screenerView .curated-detail-pane {
min-width: 0;
overflow: hidden;
border: 1px solid var(--r2-line);
border-radius: 10px;
background: #fff;
box-shadow: var(--r2-shadow);
}
#screenerView .curated-library-pane {
display: flex;
flex-direction: column;
padding: 0;
}
#screenerView .curated-library-heading {
min-height: 58px;
display: flex;
align-items: center;
padding: 9px 14px;
border: 1px solid var(--r2-line);
border-radius: 10px 10px 0 0;
border-bottom: 1px solid var(--r2-line-soft);
background: #fff;
}
#screenerView .curated-library-heading > div > span { color: var(--scr-blue); font-size: 10px; font-weight: 700; }
#screenerView .curated-library-heading h3 { display: inline; margin: 0 9px 0 0; font-size: 14px; }
#screenerView .curated-library-heading p { display: inline; margin: 0; color: var(--r2-faint); font-size: 10.5px; }
#screenerView .curated-library-heading h3 { margin: 1px 0 0; font-size: 14px; }
#screenerView .curated-library-heading > strong { margin-left: auto; color: var(--r2-sub); font-size: 11px; }
#screenerView .curated-library-controls {
min-height: 50px;
display: flex;
min-height: 48px;
display: grid;
grid-template-columns: minmax(0, 1fr) 104px 62px;
align-items: center;
gap: 12px;
margin: 0 0 12px;
padding: 8px 14px;
gap: 7px;
padding: 7px 10px;
border-bottom: 1px solid var(--r2-line-soft);
background: #fafbfc;
}
#screenerView .curated-view-toggle {
min-height: 32px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 2px;
border: 1px solid var(--r2-line);
border-top: 0;
border-radius: 0 0 10px 10px;
border-radius: 7px;
background: #fff;
}
#screenerView .curated-view-toggle button {
min-width: 0;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--r2-faint);
cursor: pointer;
}
#screenerView .curated-view-toggle button.active { background: var(--scr-blue-soft); color: var(--scr-blue); }
#screenerView .curated-view-toggle button:focus-visible { outline: 2px solid var(--scr-blue); outline-offset: 1px; }
#screenerView .curated-view-toggle .lucide { width: 13px; height: 13px; }
#screenerView .curated-school-filters {
min-height: 38px;
display: flex;
align-items: center;
gap: 4px;
padding: 5px 8px;
overflow-x: auto;
border-bottom: 1px solid var(--r2-line-soft);
background: #fff;
scrollbar-width: none;
}
#screenerView .curated-school-filters::-webkit-scrollbar { display: none; }
#screenerView .curated-school-filters button {
min-height: 26px;
display: inline-flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
padding: 0 7px;
border: 1px solid transparent;
border-radius: 5px;
background: transparent;
color: var(--r2-sub);
font-size: 10px;
cursor: pointer;
}
#screenerView .curated-school-filters button small { color: var(--r2-faint); font-size: 8.5px; font-variant-numeric: tabular-nums; }
#screenerView .curated-school-filters button:hover { background: var(--scr-blue-soft); color: var(--scr-blue); }
#screenerView .curated-school-filters button.active { border-color: #c5d4f1; background: var(--scr-blue-soft); color: var(--scr-blue); font-weight: 700; }
#screenerView .curated-school-filters button:focus-visible { outline: 2px solid var(--scr-blue); outline-offset: 1px; }
#screenerView .curated-search {
width: 240px;
width: 100%;
min-height: 32px;
display: flex;
align-items: center;
@@ -5266,51 +5342,176 @@ body.sidebar-collapsed .status-bar { left: 64px; }
#screenerView .curated-search .lucide { width: 14px; color: var(--r2-faint); }
#screenerView .curated-search input { min-width: 0; flex: 1; border: 0; outline: 0; font-size: 11.5px; }
#screenerView .curated-category-filters { display: flex; gap: 5px; overflow-x: auto; }
#screenerView .curated-category-filters button { min-height: 28px; padding: 0 10px; border: 1px solid var(--r2-line); border-radius: 6px; background: #fff; color: var(--r2-sub); font-size: 10.5px; white-space: nowrap; }
#screenerView .curated-category-filters button.active { border-color: var(--scr-blue); background: var(--scr-blue-soft); color: var(--scr-blue); font-weight: 700; }
#screenerView .curated-strategy-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 12px;
padding: 0;
#screenerView .curated-category-select {
position: relative;
min-height: 32px;
display: flex;
align-items: center;
}
#screenerView .curated-strategy-card {
min-height: 158px;
#screenerView .curated-category-select select {
width: 100%;
min-height: 32px;
padding: 0 27px 0 9px;
border: 1px solid var(--r2-line);
border-radius: 7px;
appearance: none;
background: #fff;
color: var(--r2-sub);
font-size: 10.5px;
}
#screenerView .curated-category-select .lucide {
position: absolute;
right: 8px;
width: 13px;
pointer-events: none;
color: var(--r2-faint);
}
#screenerView .curated-strategy-list {
min-height: 352px;
max-height: 532px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 13px 15px;
border: 1px solid var(--r2-line);
border-radius: 10px;
background: #fff;
box-shadow: var(--r2-shadow);
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
gap: 6px;
overflow-y: auto;
padding: 8px;
scrollbar-gutter: stable;
}
#screenerView .curated-strategy-icon { display: none; }
#screenerView .curated-strategy-list.is-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-content: start;
}
#screenerView .curated-strategy-list.is-grid .curated-strategy-card {
min-height: 124px;
align-items: center;
justify-content: center;
gap: 7px;
text-align: center;
}
#screenerView .curated-strategy-list.is-grid .curated-strategy-card:hover { transform: translateY(-2px); }
#screenerView .curated-strategy-list.is-grid .curated-strategy-card.active { box-shadow: inset 0 3px var(--scr-blue); }
#screenerView .curated-strategy-list.is-grid .curated-strategy-icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 7px;
background: var(--scr-blue-soft);
color: var(--scr-blue);
}
#screenerView .curated-strategy-list.is-grid .curated-strategy-icon .lucide { width: 16px; height: 16px; }
#screenerView .curated-strategy-list.is-grid .curated-card-head { width: 100%; grid-template-columns: minmax(0, 1fr); gap: 4px; }
#screenerView .curated-strategy-list.is-grid .curated-strategy-rank,
#screenerView .curated-strategy-list.is-grid .curated-card-tags { display: none; }
#screenerView .curated-strategy-list.is-grid .curated-card-result { justify-self: center; }
#screenerView .curated-strategy-card {
min-height: 70px;
display: flex;
flex-direction: column;
gap: 6px;
flex: 0 0 auto;
padding: 9px 10px;
border: 1px solid transparent;
border-radius: 7px;
background: #f8fafc;
box-shadow: none;
cursor: pointer;
transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease;
}
#screenerView .curated-strategy-card:hover {
border-color: #b7c9ee;
transform: translateY(-2px);
box-shadow: 0 5px 16px rgba(37, 99, 235, .08);
background: #f4f7fd;
transform: translateX(2px);
}
#screenerView .curated-strategy-card.active { border-color: #9cb5ec; background: #fbfdff; box-shadow: inset 0 3px var(--scr-blue), var(--r2-shadow); }
#screenerView .curated-card-head { display: grid; grid-template-columns: 27px minmax(0, 1fr) auto; align-items: center; gap: 8px; }
#screenerView .curated-strategy-rank { width: 27px; height: 27px; display: grid; place-items: center; border-radius: 5px; background: #f1f3f6; color: var(--r2-sub); font-size: 9.5px; font-style: normal; }
#screenerView .curated-strategy-card.active { border-color: #9cb5ec; background: var(--scr-blue-soft); box-shadow: inset 3px 0 var(--scr-blue); }
#screenerView .curated-card-head { display: grid; grid-template-columns: 25px minmax(0, 1fr) auto; align-items: center; gap: 8px; }
#screenerView .curated-strategy-rank { width: 25px; height: 25px; display: grid; place-items: center; border-radius: 5px; background: #eef1f5; color: var(--r2-sub); font-size: 9px; font-style: normal; }
#screenerView .curated-card-head strong,
#screenerView .curated-card-head small { display: block; }
#screenerView .curated-card-head strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
#screenerView .curated-card-head small { margin-top: 2px; color: var(--r2-faint); font-size: 9.5px; }
#screenerView .curated-card-head strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
#screenerView .curated-card-head small { margin-top: 1px; color: var(--r2-faint); font-size: 9px; }
#screenerView .curated-card-result { padding: 2px 6px; border-radius: 4px; background: #eef1f5; color: var(--r2-faint); font-size: 9px; font-style: normal; white-space: nowrap; }
#screenerView .curated-card-result.ready { background: var(--scr-green-soft); color: var(--scr-green); }
#screenerView .curated-card-result.quiet { background: var(--scr-blue-soft); color: var(--scr-blue); }
#screenerView .curated-card-result.missing { background: var(--scr-amber-soft); color: var(--scr-amber); }
#screenerView .curated-card-tags { display: flex; flex-wrap: wrap; gap: 4px; }
#screenerView .curated-card-tags em { padding: 2px 6px; border-radius: 4px; background: var(--scr-blue-soft); color: var(--scr-blue); font-size: 9px; font-style: normal; }
#screenerView .curated-card-tags em:nth-child(2) { background: #f3f4f6; color: var(--r2-sub); }
#screenerView .curated-card-tags em:nth-child(3) { background: var(--scr-amber-soft); color: var(--scr-amber); }
#screenerView .curated-card-description { flex: 1; color: var(--r2-sub); font-size: 10.5px; line-height: 1.65; }
#screenerView .curated-card-foot { min-height: 35px; display: flex; align-items: center; gap: 7px; padding-top: 7px; border-top: 1px solid var(--r2-line-soft); }
#screenerView .curated-card-foot small { color: var(--r2-faint); font-size: 9.5px; }
#screenerView .curated-card-actions { display: flex; gap: 5px; margin-left: auto; }
#screenerView .curated-detail-pane {
display: flex;
flex-direction: column;
padding: 14px 16px 0;
}
#screenerView .curated-detail-header { gap: 14px; padding-bottom: 11px; }
#screenerView .curated-detail-header h3 { font-size: 16px; }
#screenerView .curated-detail-header p { max-width: 650px; margin-top: 5px; font-size: 11px; line-height: 1.55; }
#screenerView .curated-strategy-badges span { min-height: 24px; padding: 0 7px; font-size: 10px; }
#screenerView .curated-environment-notes {
display: grid;
gap: 5px;
padding: 10px 0;
border-bottom: 1px solid var(--r2-line-soft);
}
#screenerView .curated-environment-notes p {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
gap: 8px;
margin: 0;
color: var(--r2-sub);
font-size: 10.5px;
line-height: 1.55;
}
#screenerView .curated-environment-notes strong { color: var(--scr-green); }
#screenerView .curated-environment-notes p:last-child strong { color: var(--scr-amber); }
#screenerView .curated-health-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 0 -16px;
border-bottom: 1px solid var(--r2-line-soft);
background: #fafbfc;
}
#screenerView .curated-health-grid > div { min-width: 0; padding: 8px 12px; border-right: 1px solid var(--r2-line-soft); }
#screenerView .curated-health-grid > div:last-child { border-right: 0; }
#screenerView .curated-health-grid span,
#screenerView .curated-health-grid strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#screenerView .curated-health-grid span { color: var(--r2-faint); font-size: 9px; }
#screenerView .curated-health-grid strong { margin-top: 2px; color: var(--r2-ink); font-size: 11.5px; font-variant-numeric: tabular-nums; }
#screenerView .curated-health-grid strong.ready { color: var(--scr-green); }
#screenerView .curated-health-grid strong.quiet { color: var(--scr-blue); }
#screenerView .curated-health-grid strong.missing { color: var(--scr-amber); }
#screenerView .curated-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; padding: 12px 0; }
#screenerView .curated-condition-section { min-width: 0; }
#screenerView .curated-rule-list,
#screenerView .curated-score-list { margin-top: 6px; }
#screenerView .curated-rule-row { min-height: 34px; padding: 5px 4px; font-size: 10.5px; }
#screenerView .curated-rule-row span,
#screenerView .curated-rule-row strong { font-size: 10.5px; }
#screenerView .curated-score-row { min-height: 31px; grid-template-columns: minmax(90px, 1fr) minmax(80px, 1.25fr) 38px; gap: 7px; font-size: 10px; }
#screenerView .curated-execution-bar { min-height: 48px; margin: auto -16px 0; padding: 7px 12px; border-top: 1px solid var(--r2-line-soft); background: #fafbfc; }
#screenerView .curated-data-status > .lucide { width: 16px; height: 16px; }
#screenerView .curated-data-status strong { font-size: 11px; }
#screenerView .curated-data-status small { margin-top: 1px; font-size: 9.5px; }
/* Quant workspace. */
#screenerView .quant-screener-panel {
@@ -5418,16 +5619,13 @@ body.sidebar-collapsed .status-bar { left: 64px; }
#screenerView .quant-validation-message { margin: 9px 14px 12px; color: var(--scr-green); font-size: 9.5px; }
#screenerView .quant-validation-message.error { color: var(--scr-amber); }
#screenerView .curated-detail-dialog { width: min(850px, calc(100vw - 30px)); max-height: min(760px, calc(100dvh - 30px)); padding: 0; overflow: visible; border: 0; border-radius: 11px; background: transparent; }
#screenerView .curated-detail-dialog::backdrop,
#screenerView .strategy-drawer::backdrop { background: rgba(20, 29, 44, .38); backdrop-filter: blur(3px); }
#screenerView .curated-detail-pane { position: relative; max-height: min(760px, calc(100dvh - 30px)); overflow-y: auto; border-radius: 11px; background: #fff; }
@media (max-width: 1180px) {
#screenerView .screener-step small { max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
#screenerView .screener-overview-grid,
#screenerView .quant-screener-panel { grid-template-columns: 1fr; }
#screenerView .curated-strategy-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
#screenerView .curated-workspace { grid-template-columns: minmax(310px, .82fr) minmax(480px, 1.3fr); }
}
@media (max-width: 820px) {
@@ -5443,9 +5641,10 @@ body.sidebar-collapsed .status-bar { left: 64px; }
#screenerView .screener-pipeline-status { margin-left: 0; }
#screenerView .screener-backtest-strip { align-items: flex-start; flex-wrap: wrap; }
#screenerView .screener-backtest-strip > p { max-width: none; margin-left: 0; }
#screenerView .curated-strategy-list { grid-template-columns: 1fr 1fr; }
#screenerView .curated-library-controls { align-items: stretch; flex-direction: column; }
#screenerView .curated-search { width: 100%; }
#screenerView .curated-workspace { grid-template-columns: 1fr; }
#screenerView .curated-strategy-list { min-height: 0; max-height: 310px; }
#screenerView .curated-strategy-list.is-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
#screenerView .curated-detail-pane { min-height: 430px; }
}
@media (max-width: 560px) {
@@ -5461,9 +5660,14 @@ body.sidebar-collapsed .status-bar { left: 64px; }
#screenerView .result-toolbar .section-subtitle { margin-left: 0; }
#screenerView .tracking-summary { grid-template-columns: repeat(2, 1fr); }
#screenerView .tracking-summary > div { border-bottom: 1px solid var(--r2-line-soft); }
#screenerView .curated-strategy-list { grid-template-columns: 1fr; }
#screenerView .curated-library-heading { align-items: flex-start; }
#screenerView .curated-library-heading p { display: block; margin-top: 2px; }
#screenerView .curated-library-controls { grid-template-columns: minmax(0, 1fr) 104px 62px; }
#screenerView .curated-strategy-list.is-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
#screenerView .curated-health-grid { grid-template-columns: 1fr 1fr; }
#screenerView .curated-health-grid > div:nth-child(2) { border-right: 0; }
#screenerView .curated-health-grid > div:nth-child(-n + 2) { border-bottom: 1px solid var(--r2-line-soft); }
#screenerView .curated-detail-grid { grid-template-columns: 1fr; }
#screenerView .curated-execution-bar { align-items: flex-start; flex-direction: column; }
#screenerView .quant-universe-grid,
#screenerView .quant-formula-summary { grid-template-columns: 1fr; }
#screenerView .quant-st-toggle { grid-column: auto; }
@@ -7911,10 +8115,13 @@ body.sidebar-collapsed .status-bar { left: 64px; }
.settings-dialog:not(.heaven-reading-dialog) {
width: min(740px, calc(100vw - 28px));
max-height: min(820px, calc(100dvh - 28px));
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.settings-dialog:not(.heaven-reading-dialog)[open] { margin: auto; }
.settings-dialog:not(.heaven-reading-dialog) .settings-section {
padding: 18px 20px;
border-color: var(--dialog-line);
@@ -8031,7 +8238,10 @@ body.sidebar-collapsed .status-bar { left: 64px; }
.admin-dialog .membership-form { grid-template-columns: 92px minmax(135px, .8fr) minmax(140px, 1fr) auto; }
/* Editing dialogs share sensible proportions without changing their fields. */
.settings-dialog.trade-log-dialog { width: min(880px, calc(100vw - 28px)); }
.settings-dialog.trade-log-dialog {
width: min(880px, calc(100vw - 28px));
max-height: min(760px, calc(100dvh - 28px));
}
.trade-log-dialog .trade-log-form { padding: 18px 20px 20px; background: #fafbfc; }
.trade-log-dialog .trade-log-form-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
.trade-log-dialog .trade-tags-field { grid-column: span 2; }
@@ -8086,6 +8296,8 @@ body.sidebar-collapsed .status-bar { left: 64px; }
}
@media (max-width: 460px) {
#screenerView .curated-library-controls { grid-template-columns: minmax(0, 1fr) 62px; }
#screenerView .curated-search { grid-column: 1 / -1; }
.global-search-dialog { margin-top: 8px; }
.global-search-head { grid-template-columns: 20px minmax(0, 1fr) 32px; padding-left: 12px; }
.global-search-head kbd { display: none; }
@@ -8337,3 +8549,94 @@ body.sidebar-collapsed .status-bar { left: 64px; }
white-space: normal;
}
}
/* Automatic screening and the manual custom-formula workspace. */
#screenerView .screener-auto-note {
color: var(--r2-faint);
font-size: 12px;
line-height: 1.6;
}
#screenerView .regime-selector .regime-option {
cursor: default;
}
#screenerView .quant-screener-panel {
grid-template-columns: minmax(360px, .88fr) minmax(0, 1.12fr);
align-items: start;
}
#screenerView .custom-screener-tools,
#screenerView .custom-results-slot {
grid-column: 1 / -1;
min-width: 0;
}
#screenerView .custom-screener-tools {
min-height: 68px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border: 1px solid var(--r2-line);
border-radius: var(--r2-radius);
background: var(--r2-card);
}
#screenerView .custom-screener-tools > div:first-child {
min-width: 0;
display: grid;
gap: 3px;
}
#screenerView .custom-screener-tools span,
#screenerView .custom-screener-tools small {
color: var(--r2-faint);
font-size: 11.5px;
}
#screenerView .custom-screener-tools strong {
color: var(--r2-ink);
font-size: 14px;
}
#screenerView .custom-screener-tools > div:last-child {
flex: 0 0 auto;
display: flex;
gap: 8px;
}
#screenerView .custom-results-slot .screener-results-view {
margin: 0;
}
#screenerView .screener-result-frame {
overflow-x: auto;
scrollbar-gutter: stable;
}
#screenerView .screener-result-frame .data-table {
width: 100%;
min-width: 1180px;
table-layout: fixed;
}
#screenerView .screener-result-columns col:nth-child(1) { width: 54px; }
#screenerView .screener-result-columns col:nth-child(2) { width: 122px; }
#screenerView .screener-result-columns col:nth-child(3) { width: 104px; }
#screenerView .screener-result-columns col:nth-child(4) { width: 78px; }
#screenerView .screener-result-columns col:nth-child(5) { width: 104px; }
#screenerView .screener-result-columns col:nth-child(6),
#screenerView .screener-result-columns col:nth-child(7),
#screenerView .screener-result-columns col:nth-child(8),
#screenerView .screener-result-columns col:nth-child(9) { width: 82px; }
#screenerView .screener-result-columns col:nth-child(10) { width: 210px; }
#screenerView .screener-result-columns col:nth-child(11) { width: 150px; }
#screenerView .screener-result-columns col:nth-child(12) { width: 142px; }
@media (max-width: 980px) {
#screenerView .quant-screener-panel { grid-template-columns: 1fr; }
#screenerView .custom-screener-tools { align-items: stretch; flex-direction: column; }
#screenerView .custom-screener-tools > div:last-child { flex-wrap: wrap; }
}
+16 -2
View File
@@ -3983,20 +3983,29 @@ dialog::backdrop {
to { transform: rotate(360deg); }
}
.toast {
#toast.toast {
position: fixed;
top: auto;
left: auto;
right: 18px;
bottom: 48px;
z-index: 60;
width: max-content;
height: auto;
max-width: min(420px, calc(100vw - 36px));
padding: 11px 14px;
border-radius: 4px;
background: #21313c;
color: #fff;
box-shadow: var(--shadow);
opacity: 1;
pointer-events: none;
transform: none;
animation: toast-enter var(--motion-medium) var(--ease-out) both;
}
#toast.toast[hidden] { display: none; }
@keyframes toast-enter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
@@ -5147,10 +5156,15 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
display: none;
}
.toast {
#toast.toast {
top: auto;
left: auto;
right: 10px;
bottom: 76px;
width: max-content;
height: auto;
max-width: calc(100vw - 20px);
transform: none;
}
}
+112 -2
View File
@@ -568,6 +568,54 @@
background: var(--action-soft);
}
:root[data-theme="dark"] #screenerView .curated-detail-pane {
background: var(--surface);
color: var(--text-primary);
}
:root[data-theme="dark"] #screenerView :is(.curated-detail-header, .curated-execution-bar) {
border-color: var(--line-soft);
}
:root[data-theme="dark"] #screenerView :is(
.curated-detail-header h3,
.mini-section-heading h4,
.curated-rule-row strong,
.curated-score-row strong,
.curated-data-status strong
) {
color: var(--text-primary);
}
:root[data-theme="dark"] #screenerView :is(
.curated-detail-header > div > span,
.curated-detail-header p,
.mini-section-heading > span,
.curated-rule-row span,
.curated-score-row > span:first-child,
.curated-data-status small
) {
color: var(--text-secondary);
}
:root[data-theme="dark"] #screenerView .curated-rule-row {
border-color: var(--line-soft);
}
:root[data-theme="dark"] #screenerView .curated-score-track {
background: var(--surface-muted);
}
:root[data-theme="dark"] #screenerView .curated-strategy-badges span {
background: var(--surface-muted);
color: var(--text-secondary);
}
:root[data-theme="dark"] #screenerView .curated-strategy-badges span:first-child {
background: var(--action-soft);
color: var(--action);
}
:root[data-theme="dark"] :is(.strategy-drawer-sidebar, .strategy-sidebar, .strategy-drawer-content) {
border-color: var(--line-soft);
background: var(--surface-muted);
@@ -1030,10 +1078,11 @@
}
:root[data-theme="dark"] #screenerView :is(
.curated-library-pane,
.curated-library-heading,
.curated-library-controls,
.curated-search,
.curated-category-filters button,
.curated-category-select select,
.curated-strategy-card,
.curated-strategy-rank,
.curated-card-tags em,
@@ -1052,7 +1101,6 @@
}
:root[data-theme="dark"] #screenerView :is(
.curated-category-filters button.active,
.curated-strategy-card.active,
.curated-card-tags em,
.curated-strategy-rank,
@@ -1061,6 +1109,30 @@
background: var(--surface-muted);
}
:root[data-theme="dark"] #screenerView .curated-strategy-card:hover {
border-color: var(--blue-line);
background: var(--surface-subtle);
}
:root[data-theme="dark"] #screenerView .curated-strategy-card.active {
border-color: var(--blue-line);
background: var(--action-soft);
}
:root[data-theme="dark"] #screenerView :is(.curated-health-grid, .curated-execution-bar) {
border-color: var(--line-soft);
background: var(--surface-muted);
}
:root[data-theme="dark"] #screenerView .curated-health-grid > div {
border-color: var(--line-soft);
}
:root[data-theme="dark"] #screenerView .curated-search input {
background: transparent;
color: var(--text-primary);
}
:root[data-theme="dark"] #screenerView :is(
.quant-rule-row select,
.quant-rule-row input,
@@ -1257,6 +1329,44 @@
color: var(--text-secondary);
}
/* High-specificity dark surfaces for workspaces with later light-theme hover rules. */
:root[data-theme="dark"] #screenerView .curated-library-heading h3 {
color: var(--text-primary);
}
:root[data-theme="dark"] #screenerView :is(.curated-view-toggle, .curated-school-filters) {
border-color: var(--border);
background: var(--surface);
color: var(--text-primary);
}
:root[data-theme="dark"] #screenerView :is(.curated-view-toggle button, .curated-school-filters button) {
color: var(--text-secondary);
}
:root[data-theme="dark"] #screenerView :is(.curated-view-toggle button, .curated-school-filters button):is(:hover, .active) {
border-color: var(--blue-line);
background: var(--action-soft);
color: var(--action);
}
:root[data-theme="dark"] #screenerView .curated-strategy-list.is-grid .curated-strategy-icon {
background: var(--action-soft);
color: var(--action);
}
:root[data-theme="dark"] #screenerView .screener-result-frame tbody tr:hover td,
:root[data-theme="dark"] #screenerView .screener-result-frame tbody tr:hover td:last-child,
:root[data-theme="dark"] #reviewWorkspaceView .data-table tbody tr:hover,
:root[data-theme="dark"] #reviewWorkspaceView .data-table tbody tr:hover td {
background: var(--action-soft) !important;
color: var(--text-primary);
}
:root[data-theme="dark"] #reviewWorkspaceView .data-table tbody td {
color: var(--text-primary);
}
/* Wentian v2 owns its complete palette in wentian-v2.css. Keeping the former
paper-theme overrides here would repaint its controls and ritual stages. */
-2
View File
@@ -614,8 +614,6 @@ button { cursor: pointer; }
.login-dialog form { display: grid; gap: 14px; padding: 26px; }
.login-dialog h2 { margin: 0 0 6px; color: var(--wt-paper); font-size: 24px; }
.login-dialog .button { width: 100%; margin-top: 4px; }
.toast { position: fixed; right: 20px; bottom: 20px; z-index: 100; padding: 10px 14px; border: 1px solid var(--wt-line); border-radius: 7px; background: rgba(13,21,38,.96); color: var(--wt-paper); box-shadow: 0 12px 30px rgba(0,0,0,.3); font-size: 12px; }
@media (max-width: 960px) {
.heaven-shell { width: min(100% - 20px, 760px); }
.heaven-controls { align-items: stretch; flex-direction: column; }