feat: Implement Alpha list and metrics enhancements
Deploy production / deploy (push) Successful in 1m12s
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:
@@ -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>
|
||||
|
||||
@@ -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 })}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user