import { WorkspaceTable } from "../components/WorkspaceTable"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Banner, Button, Checkbox, CheckboxGroup, Empty, Input, Modal, Popover, Pagination, Select, Tag, TextArea, Toast, Typography, } from "@douyinfe/semi-ui-19"; import { IconFilter, IconSetting, IconSort, IconSearch, IconClose, IconDownload, IconUpload, IconRefresh, } from "@douyinfe/semi-icons"; import "./AlphaPage.css"; import { formatAlphaMetric } from "../alphaMetrics"; import type { ColumnProps } from "@douyinfe/semi-ui-19/lib/es/table/interface"; import { api, correlationLabels, formatNumber, formatTime, patch, post, queryString, stateLabels, stateOptions, } from "../api"; import type { Account, Alpha, AlphaPage as Page, Facets, Job, Submission, } from "../types"; import { AlphaDetail } from "../components/AlphaDetail"; import { AlphaSyncDialog } from "../components/AlphaSyncDialog"; import type { PageContext, AlphaUIAction as UIAction } from "../ai/types"; import type { UIAction as WorkspaceAction } from "../ai/types"; import { SavedViews } from "../research/SavedViews"; import { sourceLabel } from "../research/SourceDetails"; const metricLabels = { sharpe: "Sharpe", fitness: "Fitness", returns: "Returns", turnover: "Turnover", margin: "Margin", drawdown: "Drawdown", sub_universe_sharpe: "子股票池 Sharpe", robust_universe_sharpe: "稳健股票池 Sharpe", two_year_sharpe: "近两年 Sharpe", prod_correlation: "平台生产相关性", pnl: "IS PnL", }; const metricColorThresholds: Record = { sharpe: [1.5, 1.0, 0.5], fitness: [0.75, 0.5, 0.25], returns: [0.15, 0.1, 0.05], }; /** Preserve the legacy list's strict thresholds; missing values have no rating. */ function metricColor(key: string, value: unknown): string | undefined { const thresholds = metricColorThresholds[key]; if (!thresholds || typeof value !== "number" || !Number.isFinite(value)) return undefined; if (value > thresholds[0]) return "var(--semi-color-success)"; if (value > thresholds[1]) return "var(--semi-color-info)"; if (value > thresholds[2]) return "var(--semi-color-warning)"; return "var(--semi-color-danger)"; } /** Format metrics only for display; filtering and sorting still use raw values. */ function formatMetric(key: string, value: unknown): string { const headline = formatAlphaMetric(key, value); if (headline !== undefined) return headline; if (key !== "margin") return formatNumber(value, 3); return typeof value === "number" && Number.isFinite(value) ? `${formatNumber(value * 10000, 2)}bps` : "—"; } 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", "correlation", "research", "source", ]; const columnLabels: Record = { selection: "Selection", combo: "Combo", component_count: "组件数(已核实)", source: "研究来源", name: "Alpha", expression: "表达式", region: "地区 / Universe", status: "平台状态", sharpe: "Sharpe", fitness: "Fitness", returns: "Returns", turnover: "Turnover", margin: "Margin", drawdown: "Drawdown", research: "研究状态", 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: "提交时间", correlation: "本地自相关", }; export function AlphaPage({ managementScope = "non_super", account, taskPanelOpen, version, onTask, onAccount, active, overlaySuspended, chatOffset, onContext, action, onOverlay, onAction, }: { managementScope?: "super" | "non_super"; account: Account | null; taskPanelOpen: boolean; version: string; onTask: () => void; onAccount: () => void; active: boolean; overlaySuspended: boolean; chatOffset: number; onContext: (context: PageContext) => void; action: UIAction | null; onOverlay: () => void; onAction: (action: WorkspaceAction) => void; }) { const columnKey = managementScope === "super" ? "superalpha-columns" : "alpha-columns"; const defaultColumns = managementScope === "super" ? initialColumns.flatMap((c) => c === "expression" ? ["selection", "combo", "component_count"] : [c], ) : initialColumns; const menuColumns = Object.fromEntries( Object.entries(columnLabels).filter(([k]) => managementScope === "super" ? k !== "expression" : !["selection", "combo", "component_count"].includes(k), ), ); const [data, setData] = useState({ items: [], total: 0, limit: 25, offset: 0, }); const [facets, setFacets] = useState({}); const [draft, setDraft] = useState>({}); const [filters, setFilters] = useState>({}); const [filterOpen, setFilterOpen] = useState(false); const [selectedViewId, setSelectedViewId] = useState(null); const [submissionBlocked, setSubmissionBlocked] = useState(false); const [submission, setSubmission] = useState("UNSUBMITTED"); const [syncingScope, setSyncingScope] = useState(null); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const [sort, setSort] = useState("date_created"); const [direction, setDirection] = useState("desc"); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [selected, setSelected] = useState([]); const [visibleColumns, setVisibleColumns] = useState(() => { try { const stored: unknown = JSON.parse( localStorage.getItem(columnKey) || "null", ); return Array.isArray(stored) ? [ ...new Set([ "name", ...stored.filter( (v): v is string => typeof v === "string" && v in columnLabels, ), ]), ] : defaultColumns; } catch { return defaultColumns; } }); const [detailId, setDetailId] = useState(null); const [importing, setImporting] = useState(false); 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 selectionVersions = useRef>({}); const [bulkVersions, setBulkVersions] = useState>({}); const actionNonce = useRef(0); const [bulk, setBulk] = useState({ add_tags: "", remove_tags: "", state: "", }); const [localVersion, setLocalVersion] = useState(0); const tablePanel = useRef(null); const [compact, setCompact] = useState( () => matchMedia("(max-width: 760px)").matches, ); useEffect(() => { const media = matchMedia("(max-width: 760px)"); const change = () => setCompact(media.matches); media.addEventListener("change", change); return () => media.removeEventListener("change", change); }, []); useEffect(() => { if (account) { setPageSize(account.page_size); setPage(1); } }, [account?.page_size]); const params = useMemo( () => ({ ...filters, management_scope: managementScope, submission, submission_blocked: submissionBlocked || undefined, sort, direction, limit: pageSize, offset: (page - 1) * pageSize, }), [ filters, submission, submissionBlocked, sort, direction, pageSize, page, managementScope, ], ); const query = queryString(params); useEffect(() => { if (!active) return; onContext({ page: managementScope === "super" ? "superalphas" : "alphas", alpha_id: detailId, selected_ids: selected.slice(0, 100), filters: Object.fromEntries( Object.entries(params).filter( ([, value]) => value !== "" && value !== undefined && value !== null, ), ), }); }, [active, detailId, selected, params, onContext]); useEffect(() => { if (!active || !action || actionNonce.current === action.nonce) return; actionNonce.current = action.nonce; if (action.type === "open_alpha") { setDetailId(action.alpha_id); onOverlay(); } else { setSelectedViewId(null); setFilterOpen(false); const { sort: nextSort, direction: nextDirection, limit: _limit, offset: _offset, submission: nextSubmission, submission_blocked: nextBlocked, ...values } = action.filters; const next = Object.fromEntries( Object.entries(values) .filter(([, v]) => v !== null && v !== undefined) .map(([k, v]) => [k, String(v)]), ); setDraft(next); setFilters(next); setPage(1); setSelected([]); if (nextSubmission === "SUBMITTED" || nextSubmission === "UNSUBMITTED") setSubmission(nextSubmission); setSubmissionBlocked(nextBlocked === true || nextBlocked === "true"); if (nextSort) setSort(String(nextSort)); if (nextDirection) setDirection(String(nextDirection)); } }, [action, onOverlay, active]); const refresh = useCallback(() => setLocalVersion((n) => n + 1), []); useEffect(() => { if (!active) return; const controller = new AbortController(); setLoading(true); setError(""); Promise.all([ api(`/alphas?${query}`, { signal: controller.signal }), api(`/alphas/facets?management_scope=${managementScope}`, { signal: controller.signal, }), ]) .then(([results, nextFacets]) => { if (!controller.signal.aborted) { setData(results); setFacets(nextFacets); } }) .catch((e) => { if (!controller.signal.aborted) setError(e.message); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [active, query, version, localVersion]); useEffect(() => { setSelected([]); selectionVersions.current = {}; }, [filters, submission, submissionBlocked, managementScope]); useEffect(() => { tablePanel.current?.querySelector(".semi-table-body")?.scrollTo({ top: 0 }); }, [query]); function updateDraft(key: string, value: unknown) { setDraft((previous) => ({ ...previous, [key]: value === undefined || value === null ? "" : String(value), })); } function applyFilters() { setFilterOpen(false); setFilters({ ...draft }); setPage(1); } function clearFilters() { setFilterOpen(false); setDraft({}); setFilters({}); setPage(1); } function changeSubmission(value: string) { setSelectedViewId(null); setFilterOpen(false); setDirection("desc"); setSubmission(value === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED"); setSubmissionBlocked(value === "SUBMISSION_BLOCKED"); setDraft({}); setFilters({}); setPage(1); setSelected([]); setSort(value === "SUBMITTED" ? "date_submitted" : "date_created"); } async function newTask(kind: string, ids: string[] = []) { setBusy(kind); try { const job = await post("/sync-jobs", { kind, alpha_ids: ids, ...(kind === "full_sync" ? { submission: "SUBMITTED" } : {}), }); setImporting(false); setIdText(""); if (kind === "pnl_backfill" && job.total === 0) Toast.success("已提交 Alpha 的 PnL 已齐全,无需补取"); if (kind === "self_correlation_recheck" && job.total === 0) Toast.success("没有预检通过或检查通过的 Alpha,无需重检"); onTask(); } catch (e) { Toast.error((e as Error).message); } finally { setBusy(""); } } async function bulkSave() { setBusy("bulk"); const split = (value: string) => value .split(/[,,]/) .map((t) => t.trim()) .filter(Boolean); try { await patch("/alphas/research/bulk", { alpha_ids: Object.keys(bulkVersions), versions: bulkVersions, add_tags: split(bulk.add_tags), remove_tags: split(bulk.remove_tags), state: bulk.state || null, }); setBulkOpen(false); setBulk({ add_tags: "", remove_tags: "", state: "" }); refresh(); Toast.success("批量研究记录已更新"); } catch (e) { Toast.error((e as Error).message); } finally { 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[] = [ { key: "name", title: "Alpha", dataIndex: "name", fixed: "left", width: 190, render: (_, row) => ( ), }, { key: "expression", title: "表达式", width: 260, render: (_, row) => ( {row!.expression_preview || "—"} ), }, ...(["selection", "combo"] as const).map( (key): ColumnProps => ({ key, title: columnLabels[key], width: 260, render: (_, row) => ( {row![`${key}_preview`] || "未提供"} ), }), ), { key: "component_count", title: "组件数(已核实)", width: 140, render: (_, row) => row!.component_count ?? "未核实", }, { key: "source", title: "研究来源", width: 150, render: (_, row) => row!.source_kinds?.map(sourceLabel).join("、") || "暂无本地来源", }, { key: "region", title: "地区 / Universe", width: 155, render: (_, row) => (
{row!.region ?? "—"} / {row!.universe ?? "—"}
), }, { key: "status", title: "平台状态", width: 150, render: (_, row) => (
{row!.status ?? "未提供"} {" "} · {row!.stage ?? "—"} {row!.hidden ? " · 隐藏" : ""}
), }, ...Object.entries(metricLabels).map( ([key, title]): ColumnProps => ({ key, title, width: key in { sub_universe_sharpe: 1, robust_universe_sharpe: 1, two_year_sharpe: 1, prod_correlation: 1, } ? 155 : 112, align: "right", render: (_, row) => ( {formatMetric(key, row![key as keyof Alpha])} ), }), ), { key: "check_type", title: "检查结果", width: 125, render: (_, row) => ( {checkLabels[row!.check_type] || "待检查"} ), }, { key: "failed_checks", title: "失败项", width: 230, render: (_, row) => ( {row!.failed_checks?.join("、") || "—"} ), }, { key: "neutralization", title: "中性化", width: 155, render: (_, row) => row!.neutralization || "—", }, { key: "research", fixed: "right", title: "研究状态", width: 115, render: (_, row) => ( {stateLabels[row!.research.state]} ), }, { key: "tags", title: "自定义标签", width: 180, render: (_, row) => (
{row!.research.tags.map((t) => ( {t} ))}
), }, { key: "language", title: "语言", width: 125, dataIndex: "language" }, { key: "correlation", title: "本地自相关", width: 160, render: (_, row) => { const value = row!.local_correlation; return value ? ( 0.75 ? "var(--semi-color-danger)" : value.max_correlation >= 0.7 ? "rgb(var(--semi-yellow-7))" : "var(--semi-color-success)", }} > {value.stale ? "待重算" : correlationLabels[value.status]} {value.max_correlation !== null && ` · ${formatNumber(value.max_correlation, 3)}`} ) : ( 未检测 ); }, }, { key: "synced", title: "同步时间", width: 165, render: (_, row) => formatTime(row!.synced_at, account?.timezone), }, { key: "submitted", title: "提交时间", width: 165, render: (_, row) => formatTime(row!.date_submitted, account?.timezone), }, { key: "created", title: "创建时间", width: 165, render: (_, row) => formatTime(row!.date_created, account?.timezone), }, ]; // Submitted alphas always expose the platform production correlation, including saved views. const displayedColumns = submission === "SUBMITTED" ? [...new Set([...visibleColumns, "prod_correlation"])] : visibleColumns; const columns = columnDefinitions .filter((column) => displayedColumns.includes(String(column.key))) .map((column) => (compact ? { ...column, fixed: undefined } : column)); const activeFilterCount = Object.values(filters).filter(Boolean).length; const options = (key: string) => (Array.isArray(facets[key]) ? (facets[key] as string[]) : []).map( (value) => ({ value, label: value }), ); const connected = account?.connection_status === "connected" || account?.connection_status === "expired"; const filterPanel = (
{ e.preventDefault(); applyFilters(); }} >

筛选 Alpha

设置搜索和筛选条件,查询后应用到当前视图。

更多筛选
{[ ["universe", "Universe"], ["alpha_type", "Alpha 类型"], ["language", "语言"], ["stage", "IS / OS"], ].map(([key, label]) => ( {Object.entries({ ...metricLabels, local_correlation: "本地自相关", }).map(([key, label]) => ( ))} updateDraft("favorite", e.target.checked ? "true" : "") } > 只看收藏

本地自相关范围为 -1~1,按缓存数值筛选,可搭配状态排除待重算记录。 数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。 检查结果仅汇总已同步的平台检查;预检通过不代表可以提交,活动资格可用自定义标签记录。

); return ( <> {!connected && ( 连接 WorldQuant 后即可同步平台数据。 } /> )} {error && }
{ setFilterOpen(false); const { submission: savedSubmission, submission_blocked: savedBlocked, sort: savedSort, direction: savedDirection, limit, offset: _offset, ...rest } = view.filters; const values = Object.fromEntries( Object.entries(rest) .filter(([, v]) => v !== null && v !== undefined) .map(([k, v]) => [k, String(v)]), ); setFilters(values); setDraft(values); setPage(1); setSelected([]); setSubmission( savedSubmission === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED", ); setSubmissionBlocked( savedBlocked === true || savedBlocked === "true", ); setSort(String(savedSort || "date_created")); setDirection(String(savedDirection || "desc")); if (typeof limit === "number") setPageSize(limit); const next = [ ...new Set([ "name", ...view.columns.filter((c) => c in columnLabels), ]), ]; setVisibleColumns(next); localStorage.setItem(columnKey, JSON.stringify(next)); }} />
显示列 ({ value, label, disabled: value === "name" || (submission === "SUBMITTED" && value === "prod_correlation"), }), )} onChange={(values) => { const next = values.map(String); setVisibleColumns(next); localStorage.setItem(columnKey, JSON.stringify(next)); }} />
} >

排序

{ setDirection(String(v)); setPage(1); }} />
} >
{submission === "SUBMITTED" && ( )} {submission === "SUBMITTED" && ( )}
{selected.length > 0 && (
已选择 {selected.length} 条
)} fill columns={columns} dataSource={data.items} rowKey="id" loading={loading} className="alpha-table" size="small" scroll={{ y: "100%", x: columns.reduce((sum, c) => sum + Number(c.width || 120), 0) + 60, }} rowSelection={{ selectedRowKeys: selected, onChange: (keys) => { const pageIds = new Set(data.items.map((r) => r.id)); const nextIds = (keys ?? []) .map(String) .filter((id) => pageIds.has(id)); for (const row of data.items) if (nextIds.includes(row.id)) selectionVersions.current[row.id] = row.research.version; setSelected((old) => [ ...old.filter((id) => !pageIds.has(id)), ...nextIds, ]); }, fixed: !compact, }} pagination={false} empty={ } />
{submissionBlocked ? "提交受阻" : submission === "UNSUBMITTED" ? "待提交" : "已提交"}{" "} Alpha{" "} {data.total.toLocaleString()}{" "} 条 最近数据更新{" "} {formatTime( typeof facets.last_sync === "string" ? facets.last_sync : null, account?.timezone, )}
{ setPageSize(size); setPage(1); }} />
setTagEditor(null)} confirmLoading={busy === "tags"} okText="保存标签" onOk={() => void saveTags()} >

用逗号分隔多个标签,例如 PPAC、待活动提交。标签保存在本地,平台同步不会覆盖。

setTagEditor((current) => current ? { ...current, text } : current, ) } />
setImporting(false)} confirmLoading={busy === "alpha_refresh"} okText="开始导入" onOk={() => { const ids = [...new Set(idText.split(/[\s,,]+/).filter(Boolean))]; if (!ids.length || ids.length > 100) { Toast.warning("每次请输入 1–100 个 Alpha ID"); return; } void newTask("alpha_refresh", ids); }} >

读取指定 Alpha 的平台详情。每次最多 100 个,使用空格、逗号或换行分隔。