feat: expand screeners and stabilize interactive feedback
This commit is contained in:
+257
-207
@@ -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) => `
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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. */
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user