feat: Implement Alpha list and metrics enhancements
Deploy production / deploy (push) Successful in 1m12s

- Added new metrics fields: sub_universe_sharpe, robust_universe_sharpe, two_year_sharpe, prod_correlation, pnl, check_type, and neutralization to the Alpha model.
- Updated snapshot_columns function to derive new metrics and check types from platform snapshots.
- Enhanced API to include failed checks and check types in responses.
- Created migration script to backfill existing Alpha records with new metrics and check types.
- Updated frontend components to display new metrics and allow editing of custom tags.
- Improved filtering and sorting capabilities for new metrics in the Alpha list.
- Added tests for new functionality including checks classification and metrics filtering.
This commit is contained in:
yuxuanhui
2026-09-09 19:02:37 +08:00
parent e57b1f7a2e
commit ba0ed9d03f
13 changed files with 678 additions and 22 deletions
+2 -2
View File
@@ -384,7 +384,7 @@ export function AlphaDetail({
/>
</label>
<label>
标签
自定义标签
<Input
aria-label="研究标签"
value={tagText}
@@ -392,7 +392,7 @@ export function AlphaDetail({
dirty.current = true;
setTagText(value);
}}
placeholder="以逗号分隔,例如 低换手, 分析师预期"
placeholder="以逗号分隔,例如 PPAC, 待活动提交"
/>
</label>
<label>
+167 -12
View File
@@ -49,12 +49,27 @@ const metricLabels = {
turnover: "Turnover",
margin: "Margin",
drawdown: "Drawdown",
sub_universe_sharpe: "子股票池 Sharpe",
robust_universe_sharpe: "稳健股票池 Sharpe",
two_year_sharpe: "近两年 Sharpe",
prod_correlation: "平台生产相关性",
pnl: "IS PnL",
};
const checkLabels = {
PENDING: "待检查",
PRE_CHECK: "预检通过",
PASS: "检查通过",
FAIL_1: "FAIL=1",
FAIL_2: "FAIL≥2",
};
const initialColumns = [
"name",
"expression",
"region",
"status",
"check_type",
"failed_checks",
"tags",
"sharpe",
"fitness",
"turnover",
@@ -75,7 +90,16 @@ const columnLabels: Record<string, string> = {
margin: "Margin",
drawdown: "Drawdown",
research: "研究状态",
tags: "本地标签",
tags: "自定义标签",
check_type: "检查结果",
failed_checks: "失败项",
neutralization: "中性化",
synced: "同步时间",
sub_universe_sharpe: metricLabels.sub_universe_sharpe,
robust_universe_sharpe: metricLabels.robust_universe_sharpe,
two_year_sharpe: metricLabels.two_year_sharpe,
prod_correlation: metricLabels.prod_correlation,
pnl: metricLabels.pnl,
language: "语言",
created: "创建时间",
submitted: "提交时间",
@@ -151,6 +175,11 @@ export function AlphaPage({
const [idText, setIdText] = useState("");
const [busy, setBusy] = useState("");
const [bulkOpen, setBulkOpen] = useState(false);
const [tagEditor, setTagEditor] = useState<{
id: string;
version: number;
text: string;
} | null>(null);
const [bulkVersions, setBulkVersions] = useState<Record<string, number>>({});
const actionNonce = useRef(0);
const [bulk, setBulk] = useState({
@@ -323,6 +352,26 @@ export function AlphaPage({
setBusy("");
}
}
async function saveTags() {
if (!tagEditor) return;
setBusy("tags");
try {
await patch(`/alphas/${tagEditor.id}/research`, {
version: tagEditor.version,
tags: tagEditor.text
.split(/[,,]/)
.map((tag) => tag.trim())
.filter(Boolean),
});
setTagEditor(null);
refresh();
Toast.success("自定义标签已保存");
} catch (e) {
Toast.error((e as Error).message);
} finally {
setBusy("");
}
}
const columnDefinitions: ColumnProps<Alpha>[] = [
{
key: "name",
@@ -343,6 +392,7 @@ export function AlphaPage({
{row!.research.favorite && <Tag size="small">收藏</Tag>}
{row!.name || row!.id}
</span>
{row!.name && <small className="muted">{row!.id}</small>}
</button>
),
},
@@ -393,7 +443,16 @@ export function AlphaPage({
([key, title]): ColumnProps<Alpha> => ({
key,
title,
width: 112,
width:
key in
{
sub_universe_sharpe: 1,
robust_universe_sharpe: 1,
two_year_sharpe: 1,
prod_correlation: 1,
}
? 155
: 112,
align: "right",
render: (_, row) => (
<span className="numeric">
@@ -402,6 +461,40 @@ export function AlphaPage({
),
}),
),
{
key: "check_type",
title: "检查结果",
width: 125,
render: (_, row) => (
<Tag
color={
row!.check_type?.startsWith("FAIL")
? "red"
: row!.check_type === "PASS"
? "green"
: "grey"
}
>
{checkLabels[row!.check_type] || "待检查"}
</Tag>
),
},
{
key: "failed_checks",
title: "失败项",
width: 230,
render: (_, row) => (
<span title={row!.failed_checks?.join("、")}>
{row!.failed_checks?.join("、") || "—"}
</span>
),
},
{
key: "neutralization",
title: "中性化",
width: 155,
render: (_, row) => row!.neutralization || "—",
},
{
key: "research",
fixed: "right",
@@ -423,13 +516,27 @@ export function AlphaPage({
},
{
key: "tags",
title: "本地标签",
title: "自定义标签",
width: 180,
render: (_, row) => (
<div className="inline-tags">
{row!.research.tags.map((t) => (
<Tag key={t}>{t}</Tag>
))}
<button
className="text-link"
aria-label={`编辑 ${row!.id} 的标签`}
onClick={() => {
setTagEditor({
id: row!.id,
version: row!.research.version,
text: row!.research.tags.join(", "),
});
onOverlay();
}}
>
编辑标签
</button>
</div>
),
},
@@ -453,6 +560,12 @@ export function AlphaPage({
);
},
},
{
key: "synced",
title: "同步时间",
width: 165,
render: (_, row) => formatTime(row!.synced_at, account?.timezone),
},
{
key: "submitted",
title: "提交时间",
@@ -553,6 +666,26 @@ export function AlphaPage({
optionList={stateOptions}
onChange={(v) => updateDraft("research_state", v)}
/>
<Select
aria-label="检查结果筛选"
placeholder="全部检查结果"
showClear
value={draft.check_type || undefined}
optionList={Object.entries(checkLabels).map(([value, label]) => ({
value,
label,
}))}
onChange={(v) => updateDraft("check_type", v)}
/>
<Select
aria-label="自定义标签筛选"
showClear
filter
value={draft.tag || undefined}
optionList={options("tags")}
placeholder="全部标签"
onChange={(v) => updateDraft("tag", v)}
/>
</div>
<details className="advanced-filters">
<summary>
@@ -614,14 +747,12 @@ export function AlphaPage({
/>
</label>
<label>
本地标签
<Select
showClear
filter
value={draft.tag || undefined}
optionList={options("tags")}
placeholder="全部标签"
onChange={(v) => updateDraft("tag", v)}
中性化
<Input
aria-label="中性化筛选"
value={draft.neutralization || ""}
placeholder="例如 INDUSTRY"
onChange={(v) => updateDraft("neutralization", v)}
/>
</label>
<label>
@@ -677,6 +808,7 @@ export function AlphaPage({
</div>
<p className="muted">
数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。
检查结果仅汇总已同步的平台检查;预检通过不代表可以提交,活动资格可用自定义标签记录。
</p>
</details>
<div className="inline-actions filter-actions">
@@ -929,6 +1061,29 @@ export function AlphaPage({
/>
</footer>
</section>
<Modal
title={`编辑自定义标签 · ${tagEditor?.id || ""}`}
visible={Boolean(tagEditor) && !overlaySuspended}
onCancel={() => setTagEditor(null)}
confirmLoading={busy === "tags"}
okText="保存标签"
onOk={() => void saveTags()}
>
<p>
用逗号分隔多个标签,例如
PPAC、待活动提交。标签保存在本地,平台同步不会覆盖。
</p>
<Input
aria-label="自定义标签"
value={tagEditor?.text || ""}
placeholder="例如 PPAC, 待活动提交"
onChange={(text) =>
setTagEditor((current) =>
current ? { ...current, text } : current,
)
}
/>
</Modal>
<Modal
title="导入 Alpha ID"
visible={importing && !overlaySuspended}
@@ -967,7 +1122,7 @@ export function AlphaPage({
添加标签
<Input
aria-label="批量添加标签"
placeholder="用逗号分隔"
placeholder="用逗号分隔,例如 PPAC, 待活动提交"
value={bulk.add_tags}
onChange={(add_tags) => setBulk({ ...bulk, add_tags })}
/>
+3
View File
@@ -301,6 +301,9 @@ label:not(.semi-checkbox):not(.semi-radio) {
.columns-picker {
padding: var(--space-3);
min-width: 192px;
max-height: min(420px, 50dvh);
overflow-y: auto;
overscroll-behavior: contain;
}
.columns-picker > strong {
display: block;
+8
View File
@@ -55,6 +55,14 @@ export type Alpha = {
date_created: string | null;
date_submitted: string | null;
synced_at: string;
check_type: "PENDING" | "PRE_CHECK" | "PASS" | "FAIL_1" | "FAIL_2";
failed_checks: string[];
neutralization: string | null;
sub_universe_sharpe: number | null;
robust_universe_sharpe: number | null;
two_year_sharpe: number | null;
prod_correlation: number | null;
pnl: number | null;
research: Research;
local_correlation: CorrelationSummary | null;
source_kinds: string[];
+9 -2
View File
@@ -245,10 +245,17 @@ test("batch tags, column visibility, server pagination and saved preferences", a
await page.getByRole("button", { name: "显示列设置" }).click();
await page
.locator(".columns-picker")
.getByText("本地标签", { exact: true })
.getByText("自定义标签", { exact: true })
.click();
await expect(
page.getByRole("columnheader", { name: "本地标签", exact: true }),
page.getByRole("columnheader", { name: "自定义标签", exact: true }),
).toHaveCount(0);
await page
.locator(".columns-picker")
.getByText("自定义标签", { exact: true })
.click();
await expect(
page.getByRole("columnheader", { name: "自定义标签", exact: true }),
).toBeVisible();
await page.getByRole("button", { name: "显示列设置" }).click();
await page.getByRole("button", { name: "重置", exact: true }).click();