import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Banner, Button, Checkbox, CheckboxGroup, Empty, Input, Modal, Popover, Select, Table, Tag, TextArea, Toast, } from "@douyinfe/semi-ui-19"; import { IconSearch, IconRefresh, IconDownload, IconPlus, IconSetting, IconStar, } from "@douyinfe/semi-icons"; import type { ColumnProps } from "@douyinfe/semi-ui-19/lib/es/table/interface"; import { api, formatNumber, formatTime, patch, post, queryString, stateLabels, stateOptions, } from "../api"; import type { Account, Alpha, AlphaPage as Page, Facets } from "../types"; import { AlphaDetail } from "../components/AlphaDetail"; import type { PageContext, UIAction } from "../ai/types"; const metricLabels = { sharpe: "Sharpe", fitness: "Fitness", returns: "Returns", turnover: "Turnover", margin: "Margin", drawdown: "Drawdown", }; const initialColumns = [ "name", "expression", "region", "status", "sharpe", "fitness", "turnover", "research", ]; const columnLabels: Record = { name: "Alpha", expression: "表达式", region: "地区 / Universe", status: "平台状态", sharpe: "Sharpe", fitness: "Fitness", returns: "Returns", turnover: "Turnover", margin: "Margin", drawdown: "Drawdown", research: "研究状态", tags: "本地标签", language: "语言", created: "创建时间", }; export function AlphaPage({ account, taskPanelOpen, version, onTask, onAccount, active, overlaySuspended, chatOffset, onContext, action, onOverlay, }: { 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; }) { const [data, setData] = useState({ items: [], total: 0, limit: 25, offset: 0, }); const [facets, setFacets] = useState({}); const [draft, setDraft] = useState>({}); const [filters, setFilters] = useState>({}); 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("alpha-columns") || "null", ); return Array.isArray(stored) ? [ ...new Set([ "name", ...stored.filter( (v): v is string => typeof v === "string" && v in columnLabels, ), ]), ] : initialColumns; } catch { return initialColumns; } }); const [detailId, setDetailId] = useState(null); const [importing, setImporting] = useState(false); const [idText, setIdText] = useState(""); const [busy, setBusy] = useState(""); const [bulkOpen, setBulkOpen] = useState(false); const [bulkVersions, setBulkVersions] = useState>({}); const actionNonce = useRef(0); const [bulk, setBulk] = useState({ add_tags: "", remove_tags: "", state: "", }); const [localVersion, setLocalVersion] = useState(0); useEffect(() => { if (account) { setPageSize(account.page_size); setPage(1); } }, [account?.page_size]); const params = useMemo( () => ({ ...filters, sort, direction, limit: pageSize, offset: (page - 1) * pageSize, }), [filters, sort, direction, pageSize, page], ); const query = queryString(params); useEffect(() => { if (!active) return; onContext({ page: "alphas", alpha_id: detailId, selected_ids: selected, filters: Object.fromEntries( Object.entries(params).filter( ([, value]) => value !== "" && value !== undefined && value !== null, ), ), }); }, [active, detailId, selected, params, onContext]); useEffect(() => { if (!action || actionNonce.current === action.nonce) return; actionNonce.current = action.nonce; if (action.type === "open_alpha") { setDetailId(action.alpha_id); onOverlay(); } else { const { sort: nextSort, direction: nextDirection, limit: _limit, offset: _offset, ...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 (nextSort) setSort(String(nextSort)); if (nextDirection) setDirection(String(nextDirection)); } }, [action, onOverlay]); const refresh = useCallback(() => setLocalVersion((n) => n + 1), []); useEffect(() => { const controller = new AbortController(); setLoading(true); setError(""); Promise.all([ api(`/alphas?${query}`, { signal: controller.signal }), api("/alphas/facets", { 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(); }, [query, version, localVersion]); useEffect(() => { setSelected([]); }, [query]); function updateDraft(key: string, value: unknown) { setDraft((previous) => ({ ...previous, [key]: value === undefined || value === null ? "" : String(value), })); } function applyFilters() { setFilters({ ...draft }); setPage(1); } function clearFilters() { setDraft({}); setFilters({}); setPage(1); } async function newTask(kind: string, ids: string[] = []) { setBusy(kind); try { await post("/sync-jobs", { kind, alpha_ids: ids }); setImporting(false); setIdText(""); 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(""); } } 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 || "—"} ), }, { 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: 112, align: "right", render: (_, row) => ( {formatNumber(row![key as keyof Alpha], key === "margin" ? 6 : 3)} ), }), ), { 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: "created", title: "创建时间", width: 165, render: (_, row) => formatTime(row!.date_created, account?.timezone), }, ]; const columns = columnDefinitions.filter((column) => visibleColumns.includes(String(column.key)), ); 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"; return ( <>
ALPHA LIBRARY

Alpha 管理

集中查看平台信号,记录每一步研究判断。

已同步 Alpha {Number(facets.total ?? 0).toLocaleString()}
我的收藏 {Number(facets.favorites ?? 0).toLocaleString()}
覆盖地区 {Array.isArray(facets.region) ? facets.region.length : 0}
最近数据更新 {formatTime( typeof facets.last_sync === "string" ? facets.last_sync : null, account?.timezone, )}
{!connected && ( 连接 WorldQuant 后即可同步平台数据。 } /> )} {error && }
{ e.preventDefault(); applyFilters(); }} >
} placeholder="搜索 ID、名称或表达式" value={draft.q ?? ""} onChange={(v) => updateDraft("q", v)} showClear /> updateDraft("status", v)} /> updateDraft(key, v)} /> ))} {Object.entries(metricLabels).map(([key, label]) => ( ))} updateDraft("favorite", e.target.checked ? "true" : "") } > 只看收藏

数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。

全部 Alpha {data.total.toLocaleString()} {selected.length > 0 && ( 已选择 {selected.length} 条 )}
{selected.length > 0 && ( <> )} { setDirection(String(v)); setPage(1); }} /> 显示列 ({ value, label, disabled: value === "name", }), )} onChange={(values) => { const next = values.map(String); setVisibleColumns(next); localStorage.setItem( "alpha-columns", JSON.stringify(next), ); }} />
} >
columns={columns} dataSource={data.items} rowKey="id" loading={loading} size="middle" scroll={{ y: "max(260px, calc(100vh - 670px))", x: columns.reduce((sum, c) => sum + Number(c.width || 120), 0) + 60, }} rowSelection={{ selectedRowKeys: selected, onChange: (keys) => setSelected((keys ?? []).map(String)), fixed: true, }} pagination={{ currentPage: page, pageSize, total: data.total, showSizeChanger: true, pageSizeOpts: [25, 50, 100], onPageChange: (next) => setPage(next), onPageSizeChange: (size) => { setPageSize(size); setPage(1); }, }} empty={ α} title={ activeFilterCount ? "没有符合条件的 Alpha" : "从第一个 Alpha 开始" } description={ activeFilterCount ? "调整筛选条件,探索更多信号。" : "连接 WorldQuant 并同步数据,或通过 Alpha ID 导入已有研究。" } /> } />

平台数据与本地研究记录独立保存 · 记录不因再次同步而覆盖

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 个,使用空格、逗号或换行分隔。